diff --git a/packages/web3/src/api/node-provider.ts b/packages/web3/src/api/node-provider.ts index e6b23fe16..935c27568 100644 --- a/packages/web3/src/api/node-provider.ts +++ b/packages/web3/src/api/node-provider.ts @@ -28,7 +28,6 @@ import { requestWithLog } from './types' import { Api as NodeApi, CallContractFailed, CallContractSucceeded } from './api-alephium' -import { tryGetCallResult } from '../contract' import { HexString, addressFromContractId, @@ -38,6 +37,7 @@ import { isHexString, toNonNegativeBigInt } from '../utils' +import * as node from '../api/api-alephium' function initializeNodeApi(baseUrl: string, apiKey?: string, customFetch?: typeof fetch): NodeApi { const nodeApi = new NodeApi({ @@ -253,3 +253,10 @@ export class NodeProvider implements NodeProviderApis { } } } + +export function tryGetCallResult(result: node.CallContractResult): node.CallContractSucceeded { + if (result.type === 'CallContractFailed') { + throw new Error(`Failed to call contract, error: ${(result as node.CallContractFailed).error}`) + } + return result as node.CallContractSucceeded +} diff --git a/packages/web3/src/api/utils.ts b/packages/web3/src/api/utils.ts index 88cb848ff..30b7be173 100644 --- a/packages/web3/src/api/utils.ts +++ b/packages/web3/src/api/utils.ts @@ -17,7 +17,7 @@ along with the library. If not, see . */ import 'cross-fetch/polyfill' -import { node } from '..' +import * as node from '../api/api-alephium' export function convertHttpResponse(response: { status: number; data: T; error?: { detail: string } }): T { if (response.error) { diff --git a/packages/web3/src/codec/contract-output-codec.ts b/packages/web3/src/codec/contract-output-codec.ts index dcf86c970..ab75420b3 100644 --- a/packages/web3/src/codec/contract-output-codec.ts +++ b/packages/web3/src/codec/contract-output-codec.ts @@ -24,7 +24,7 @@ import { Codec } from './codec' import { Token, tokensCodec } from './token-codec' import { ContractOutput as ApiContractOutput } from '../api/api-alephium' import { blakeHash, createHint } from './hash' -import { binToHex, bs58 } from '..' +import { binToHex, bs58 } from '../utils' import { signedIntCodec } from './signed-int-codec' import { lockupScriptCodec } from './lockup-script-codec' diff --git a/packages/web3/src/codec/lockup-script-codec.ts b/packages/web3/src/codec/lockup-script-codec.ts index c98cfddfd..644a5686d 100644 --- a/packages/web3/src/codec/lockup-script-codec.ts +++ b/packages/web3/src/codec/lockup-script-codec.ts @@ -17,7 +17,7 @@ along with the library. If not, see . */ import { Buffer } from 'buffer/' import { Parser } from 'binary-parser' -import { DecodedCompactInt, compactUnsignedIntCodec } from './compact-int-codec' +import { DecodedCompactInt, compactSignedIntCodec } from './compact-int-codec' import { Codec } from './codec' import { ArrayCodec, DecodedArray } from './array-codec' @@ -41,7 +41,7 @@ const publicKeyHashCodec = new PublicKeyHashCodec() const publicKeyHashesCodec = new ArrayCodec(publicKeyHashCodec) const multiSigParser = Parser.start() .nest('publicKeyHashes', { type: publicKeyHashesCodec.parser }) - .nest('m', { type: compactUnsignedIntCodec.parser }) + .nest('m', { type: compactSignedIntCodec.parser }) export interface MultiSig { publicKeyHashes: DecodedArray m: DecodedCompactInt @@ -79,7 +79,7 @@ export class LockupScriptCodec implements Codec { result.push(...(input.script as PublicKeyHash).publicKeyHash) } else if (input.scriptType === 1) { result.push(...publicKeyHashesCodec.encode((input.script as MultiSig).publicKeyHashes.value)) - result.push(...compactUnsignedIntCodec.encode((input.script as MultiSig).m)) + result.push(...compactSignedIntCodec.encode((input.script as MultiSig).m)) } else if (input.scriptType === 2) { result.push(...(input.script as P2SH).scriptHash) } else if (input.scriptType === 3) { diff --git a/packages/web3/src/codec/transaction-codec.ts b/packages/web3/src/codec/transaction-codec.ts index d48790a0f..4acc2d36e 100644 --- a/packages/web3/src/codec/transaction-codec.ts +++ b/packages/web3/src/codec/transaction-codec.ts @@ -27,7 +27,7 @@ import { Either } from './either-codec' import { AssetOutput, AssetOutputCodec } from './asset-output-codec' import { ContractOutput, ContractOutputCodec } from './contract-output-codec' import { FixedAssetOutput, Transaction as ApiTransaction } from '../api/api-alephium' -import { hexToBinUnsafe } from '..' +import { hexToBinUnsafe } from '../utils' import { ContractOutput as ApiContractOutput } from '../api/api-alephium' import { Codec } from './codec' import { Output, outputCodec, outputsCodec } from './output-codec' diff --git a/packages/web3/src/contract/contract.ts b/packages/web3/src/contract/contract.ts index 0cb6b37b5..ff9bf9e33 100644 --- a/packages/web3/src/contract/contract.ts +++ b/packages/web3/src/contract/contract.ts @@ -34,7 +34,8 @@ import { getDefaultPrimitiveValue, PrimitiveTypes, decodeArrayType, - fromApiPrimitiveVal + fromApiPrimitiveVal, + tryGetCallResult } from '../api' import { CompileProjectResult } from '../api/api-alephium' import { @@ -2369,10 +2370,3 @@ export const getContractIdFromUnsignedTx = async ( // This function only works in the simple case where a single non-subcontract is created in the tx export const getTokenIdFromUnsignedTx = getContractIdFromUnsignedTx - -export function tryGetCallResult(result: node.CallContractResult): node.CallContractSucceeded { - if (result.type === 'CallContractFailed') { - throw new Error(`Failed to call contract, error: ${(result as node.CallContractFailed).error}`) - } - return result as node.CallContractSucceeded -} diff --git a/packages/web3/src/contract/events.ts b/packages/web3/src/contract/events.ts index 976006dd5..5513eca61 100644 --- a/packages/web3/src/contract/events.ts +++ b/packages/web3/src/contract/events.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License along with the library. If not, see . */ -import { web3 } from '..' +import * as web3 from '../global' import { node } from '../api' import { Subscription, SubscribeOptions } from '../utils' diff --git a/packages/web3/src/contract/ralph.test.ts b/packages/web3/src/contract/ralph.test.ts index 3d535a787..1ac32f46e 100644 --- a/packages/web3/src/contract/ralph.test.ts +++ b/packages/web3/src/contract/ralph.test.ts @@ -19,7 +19,7 @@ along with the library. If not, see . import * as ralph from './ralph' import * as utils from '../utils' import { Fields, FieldsSig, Struct, fromApiArray, fromApiEventFields, fromApiFields, getDefaultValue } from './contract' -import { node } from '..' +import * as node from '../api/api-alephium' describe('contract', function () { it('should encode I256', () => { diff --git a/packages/web3/src/signer/tx-builder.ts b/packages/web3/src/signer/tx-builder.ts index df108870b..e04fdde85 100644 --- a/packages/web3/src/signer/tx-builder.ts +++ b/packages/web3/src/signer/tx-builder.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License along with the library. If not, see . */ -import { utils } from '..' +import { binToHex, contractIdFromAddress } from '../utils' import { fromApiNumber256, node, NodeProvider, toApiNumber256Optional, toApiTokens } from '../api' import { addressFromPublicKey } from '../utils' import { toApiDestinations } from './signer' @@ -90,7 +90,7 @@ export abstract class TransactionBuilder { ...rest } const response = await this.nodeProvider.contracts.postContractsUnsignedTxDeployContract(data) - const contractId = utils.binToHex(utils.contractIdFromAddress(response.contractAddress)) + const contractId = binToHex(contractIdFromAddress(response.contractAddress)) return { ...response, groupIndex: response.fromGroup, contractId, gasPrice: fromApiNumber256(response.gasPrice) } } diff --git a/packages/web3/src/transaction/status.ts b/packages/web3/src/transaction/status.ts index b0e38ee72..24a54217e 100644 --- a/packages/web3/src/transaction/status.ts +++ b/packages/web3/src/transaction/status.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License along with the library. If not, see . */ -import { web3 } from '..' +import * as web3 from '../global' import { node } from '../api' import { Subscription, SubscribeOptions } from '../utils' diff --git a/packages/web3/src/utils/address.test.ts b/packages/web3/src/utils/address.test.ts index 39bcbd9e5..414b8a058 100644 --- a/packages/web3/src/utils/address.test.ts +++ b/packages/web3/src/utils/address.test.ts @@ -46,6 +46,10 @@ describe('address', function () { expect(() => validateAddress('eBrjfQNeyUCuxE4zpbfMZcbS3PuvbMJDQBCyk4HRHtX')).toThrow('Invalid address:') expect(validateAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9kj')).toBeUndefined() expect(() => validateAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9k')).toThrow('Invalid address:') + expect(() => validateAddress('asd')).toThrow('Invalid multisig address: asd') + expect(() => + validateAddress('2jVWAcAPphJ8ueZNG1BPwbfPFjjbvorprceuqzgmJQ1ZRyELRpWgARvdB3T9trqpiJs7f4GkudPt6rQLnGbQYqq2NCi') + ).toThrow('Invalid multisig address, n: 2, m: 3') }) it('should get address type', () => { diff --git a/packages/web3/src/utils/address.ts b/packages/web3/src/utils/address.ts index fa8d780fa..e5b780896 100644 --- a/packages/web3/src/utils/address.ts +++ b/packages/web3/src/utils/address.ts @@ -24,6 +24,9 @@ import bs58 from './bs58' import djb2 from './djb2' import { binToHex, hexToBinUnsafe } from './utils' import { KeyType } from '../signer' +import { MultiSig, lockupScriptCodec } from '../codec/lockup-script-codec' +import { Buffer } from 'buffer/' +import { compactSignedIntCodec } from '../codec' const ec = new EC('secp256k1') @@ -49,8 +52,18 @@ function decodeAndValidateAddress(address: string): Uint8Array { if (decoded.length === 0) throw new Error('Address is empty') const addressType = decoded[0] if (addressType === AddressType.P2MPKH) { - // [1, n, ...hashes, m] - if ((decoded.length - 3) % 32 === 0) return decoded + let multisig: MultiSig + try { + multisig = lockupScriptCodec.decode(Buffer.from(decoded)).script as MultiSig + } catch (_) { + throw new Error(`Invalid multisig address: ${address}`) + } + const n = multisig.publicKeyHashes.value.length + const m = compactSignedIntCodec.toI32(multisig.m) + if (n < m) { + throw new Error(`Invalid multisig address, n: ${n}, m: ${m}`) + } + return decoded } else if (addressType === AddressType.P2PKH || addressType === AddressType.P2SH || addressType === AddressType.P2C) { // [type, ...hash] if (decoded.length === 33) return decoded diff --git a/packages/web3/src/utils/exchange.test.ts b/packages/web3/src/utils/exchange.test.ts index 631ce4caf..8dbde49f8 100644 --- a/packages/web3/src/utils/exchange.test.ts +++ b/packages/web3/src/utils/exchange.test.ts @@ -29,15 +29,26 @@ import { NodeProvider } from '../api' describe('exchange', function () { it('should get address from unlock script', () => { + expect(() => + getAddressFromUnlockScript('0003498dc83e77e9b5c82b88e2bba7c737fd5aee041dc6bbb4402fefa3e7460a95bz') + ).toThrow(`expected a hex string`) expect(getAddressFromUnlockScript('0003498dc83e77e9b5c82b88e2bba7c737fd5aee041dc6bbb4402fefa3e7460a95bb')).toEqual( '18Y5mtrpu9kaEW9PoyipNQcFwVtA8X5yrGYhTZwYBwXHN' ) + expect(() => + getAddressFromUnlockScript('0003498dc83e77e9b5c82b88e2bba7c737fd5aee041dc6bbb4402fefa3e7460a95') + ).toThrow('Invalid p2pkh unlock script') expect( getAddressFromUnlockScript( '0201010000000004581440200000000000000000000000000000000000000000000000000000000000000000868500' ) ).toEqual('qCG5ZXg3b7AuGDS4HoEAhzqhCc2yxMqBYjYimBj1QFFT') + expect(() => + getAddressFromUnlockScript( + '02010100000000045814402000000000000000000000000000000000000000000000000000000000000000008685' + ) + ).toThrow('Invalid p2sh unlock script') expect(() => getAddressFromUnlockScript( diff --git a/packages/web3/src/utils/exchange.ts b/packages/web3/src/utils/exchange.ts index 58ccb4de3..d91859e63 100644 --- a/packages/web3/src/utils/exchange.ts +++ b/packages/web3/src/utils/exchange.ts @@ -16,9 +16,20 @@ You should have received a copy of the GNU Lesser General Public License along with the library. If not, see . */ -import { AddressType, addressFromPublicKey, addressFromScript, binToHex, bs58, hexToBinUnsafe } from '..' +import { + AddressType, + addressFromPublicKey, + addressFromScript, + binToHex, + bs58, + hexToBinUnsafe, + isHexString +} from '../utils' import { Transaction } from '../api/api-alephium' import { Address } from '../signer' +import { P2SH, unlockScriptCodec } from '../codec/unlock-script-codec' +import { Buffer } from 'buffer/' +import { scriptCodec } from '../codec/script-codec' export function validateExchangeAddress(address: string) { let decoded: Uint8Array @@ -80,24 +91,35 @@ enum UnlockScriptType { } export function getAddressFromUnlockScript(unlockScript: string): Address { + if (!isHexString(unlockScript)) { + throw new Error(`Invalid unlock script ${unlockScript}, expected a hex string`) + } const decoded = hexToBinUnsafe(unlockScript) if (decoded.length === 0) throw new Error('UnlockScript is empty') const unlockScriptType = decoded[0] const unlockScriptBody = decoded.slice(1) if (unlockScriptType === UnlockScriptType.P2PKH) { + if (unlockScriptBody.length !== 33) { + throw new Error(`Invalid p2pkh unlock script: ${unlockScript}`) + } return addressFromPublicKey(binToHex(unlockScriptBody)) - } else if (unlockScriptType === UnlockScriptType.P2MPKH) { + } + + if (unlockScriptType === UnlockScriptType.P2MPKH) { throw new Error('Naive multi-sig address is not supported for exchanges as it will be replaced by P2SH') - } else if (unlockScriptType === UnlockScriptType.P2SH) { - // FIXEME: for now we assume that the params is empty, so we need to - // remove the last byte from the `unlockScriptBody`, we can decode - // the unlock script once the codec PR is merged - const script = unlockScriptBody.slice(0, -1) - return addressFromScript(script) - } else { - throw new Error('Invalid unlock script type') } + + if (unlockScriptType === UnlockScriptType.P2SH) { + let p2sh: P2SH + try { + p2sh = unlockScriptCodec.decode(Buffer.from(decoded)).script as P2SH + } catch (_) { + throw new Error(`Invalid p2sh unlock script: ${unlockScript}`) + } + return addressFromScript(scriptCodec.encode(p2sh.script)) + } + throw new Error('Invalid unlock script type') } function checkALPHOutput(tx: Transaction): boolean { diff --git a/packages/web3/src/utils/number.ts b/packages/web3/src/utils/number.ts index ae196c87a..6de41cf31 100644 --- a/packages/web3/src/utils/number.ts +++ b/packages/web3/src/utils/number.ts @@ -21,7 +21,7 @@ along with the library. If not, see . // 2. https://github.com/ethers-io/ethers.js/blob/724881f34d428406488a1c9f9dbebe54b6edecda/src.ts/utils/fixednumber.ts import BigNumber from 'bignumber.js' -import { Number256 } from '..' +import { Number256 } from '../api/types' export const isNumeric = (numToCheck: any): boolean => !isNaN(parseFloat(numToCheck)) && isFinite(numToCheck) diff --git a/packages/web3/src/utils/sign.ts b/packages/web3/src/utils/sign.ts index d1a979175..14706d6d6 100644 --- a/packages/web3/src/utils/sign.ts +++ b/packages/web3/src/utils/sign.ts @@ -17,7 +17,7 @@ along with the library. If not, see . */ import { ec as EC } from 'elliptic' -import { binToHex, encodeSignature, hexToBinUnsafe, signatureDecode } from '..' +import { binToHex, encodeSignature, hexToBinUnsafe, signatureDecode } from '../utils' import { KeyType } from '../signer' import * as necc from '@noble/secp256k1' import { createHash, createHmac } from 'crypto'