diff --git a/packages/walletconnect/src/provider.ts b/packages/walletconnect/src/provider.ts index 8027aa071..4a1b4735f 100644 --- a/packages/walletconnect/src/provider.ts +++ b/packages/walletconnect/src/provider.ts @@ -47,6 +47,7 @@ import { ApiRequestArguments, NetworkId, networkIds, + keyTypes, EnableOptionsBase } from '@alephium/web3' @@ -547,12 +548,13 @@ export function formatAccount(permittedChain: string, account: Account): string } export function parseAccount(account: string): Account & { networkId: NetworkId } { - const [_namespace, networkId, _group, publicKey, keyType] = account.replace(/\//g, ':').split(':') - const address = addressFromPublicKey(publicKey) - const group = groupOfAddress(address) - if (keyType !== 'default' && keyType !== 'bip340-schnorr') { - throw Error(`Invalid key type: ${keyType}`) + const [_namespace, networkId, _group, publicKey, _keyType] = account.replace(/\//g, ':').split(':') + const keyType = keyTypes.find((tpe) => tpe === _keyType) + if (keyType === undefined) { + throw Error(`Invalid key type: ${_keyType}`) } + const address = addressFromPublicKey(publicKey, keyType) + const group = groupOfAddress(address) return { address, group, publicKey, keyType, networkId: networkId as NetworkId } } 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..b705481c2 100644 --- a/packages/web3/src/codec/lockup-script-codec.ts +++ b/packages/web3/src/codec/lockup-script-codec.ts @@ -38,7 +38,7 @@ class PublicKeyHashCodec implements Codec { } const publicKeyHashCodec = new PublicKeyHashCodec() -const publicKeyHashesCodec = new ArrayCodec(publicKeyHashCodec) +export const publicKeyHashesCodec = new ArrayCodec(publicKeyHashCodec) const multiSigParser = Parser.start() .nest('publicKeyHashes', { type: publicKeyHashesCodec.parser }) .nest('m', { type: compactUnsignedIntCodec.parser }) 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/signer/types.ts b/packages/web3/src/signer/types.ts index 86fc65579..425e0058b 100644 --- a/packages/web3/src/signer/types.ts +++ b/packages/web3/src/signer/types.ts @@ -31,9 +31,10 @@ export interface Destination { lockTime?: number message?: string } -assertType> +assertType>() -export type KeyType = 'default' | 'bip340-schnorr' +export const keyTypes = ['default', 'bip340-schnorr', 'multisig'] as const +export type KeyType = (typeof keyTypes)[number] export interface Account { keyType: KeyType 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..e195cfe65 100644 --- a/packages/web3/src/utils/address.test.ts +++ b/packages/web3/src/utils/address.test.ts @@ -27,7 +27,8 @@ import { tokenIdFromAddress, validateAddress, isAssetAddress, - isContractAddress + isContractAddress, + encodeMultisigPublicKeys } from './address' import { binToHex } from './utils' @@ -118,6 +119,54 @@ describe('address', function () { ) }) + it('should encode multisig public keys', () => { + expect(() => encodeMultisigPublicKeys([], 2)).toThrow('Public key array is empty') + expect(() => + encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'], 2) + ).toThrow('Invalid m in m-of-n multisig, m: 2, n: 1') + expect(() => + encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc', '0011'], 1) + ).toThrow('Invalid public key: 0011') + expect(encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'], 1)).toEqual( + '0101030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc' + ) + expect( + encodeMultisigPublicKeys( + [ + '030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc', + '03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19' + ], + 1 + ) + ).toEqual( + '0102030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19' + ) + expect( + encodeMultisigPublicKeys( + [ + '030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc', + '03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19', + '03c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172' + ], + 2 + ) + ).toEqual( + '0203030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be1903c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172' + ) + expect( + encodeMultisigPublicKeys( + [ + '030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc', + '03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19', + '03c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172' + ], + 3 + ) + ).toEqual( + '0303030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be1903c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172' + ) + }) + it('should compute address from public key', () => { expect(publicKeyFromPrivateKey('91411e484289ec7e8b3058697f53f9b26fa7305158b4ef1a81adfbabcf090e45')).toBe( '030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc' @@ -125,6 +174,33 @@ describe('address', function () { expect(addressFromPublicKey('030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc')).toBe( '1ACCkgFfmTif46T3qK12znuWjb5Bk9jXpqaeWt2DXx8oc' ) + const publicKeys = [ + '043ed1a15fa4b9c92d5f0b712c238cc26e16bfe8a359d6dc0aeffed983c02e800b', + 'bcdfb4cbd7555f8df4b66414f17e81eaa108dea382dabb01a63ced575d1824b37e', + '94438313828b1b17e5d7f2c9d773d44a81af6c3ef67446fbf350497ff3b06c3741' + ] + expect(() => addressFromPublicKey('0100', 'multisig')).toThrow('Invalid n in m-of-n multisig, m: 1, n: 0') + expect(() => addressFromPublicKey('013f', 'multisig')).toThrow('Invalid n in m-of-n multisig, m: 1, n: -1') + expect(() => addressFromPublicKey('0201', 'multisig')).toThrow('Invalid m in m-of-n multisig, m: 2, n: 1') + expect(() => addressFromPublicKey('3f02', 'multisig')).toThrow('Invalid m in m-of-n multisig, m: -1, n: 2') + expect(() => addressFromPublicKey('04' + encodeMultisigPublicKeys(publicKeys, 3).slice(2), 'multisig')).toThrow( + 'Invalid m in m-of-n multisig, m: 4, n: 3' + ) + expect(() => addressFromPublicKey('00' + encodeMultisigPublicKeys(publicKeys, 3).slice(2), 'multisig')).toThrow( + 'Invalid m in m-of-n multisig, m: 0, n: 3' + ) + expect(() => addressFromPublicKey(encodeMultisigPublicKeys(publicKeys, 3).slice(0, -2), 'multisig')).toThrow( + 'Invalid public key size' + ) + expect(addressFromPublicKey(encodeMultisigPublicKeys(publicKeys, 3), 'multisig')).toEqual( + 'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEMG' + ) + expect(addressFromPublicKey(encodeMultisigPublicKeys(publicKeys, 2), 'multisig')).toEqual( + 'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEMF' + ) + expect(addressFromPublicKey(encodeMultisigPublicKeys(publicKeys, 1), 'multisig')).toEqual( + 'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEME' + ) }) it('should convert between contract id and address', () => { diff --git a/packages/web3/src/utils/address.ts b/packages/web3/src/utils/address.ts index fa8d780fa..5e86eeb3c 100644 --- a/packages/web3/src/utils/address.ts +++ b/packages/web3/src/utils/address.ts @@ -22,10 +22,14 @@ import { TOTAL_NUMBER_OF_GROUPS } from '../constants' import blake from 'blakejs' import bs58 from './bs58' import djb2 from './djb2' -import { binToHex, hexToBinUnsafe } from './utils' +import { binToHex, hexToBinUnsafe, isHexString } from './utils' import { KeyType } from '../signer' +import { compactSignedIntCodec } from '../codec' +import * as codec from '../codec' +import { Buffer } from 'buffer/' const ec = new EC('secp256k1') +const PublicKeyBytesLength = 33 export enum AddressType { P2PKH = 0x00, @@ -154,10 +158,65 @@ export function addressFromPublicKey(publicKey: string, _keyType?: KeyType): str const hash = Buffer.from(blake.blake2b(Buffer.from(publicKey, 'hex'), undefined, 32)) const bytes = Buffer.concat([addressType, hash]) return bs58.encode(bytes) - } else { + } else if (keyType === 'bip340-schnorr') { const lockupScript = Buffer.from(`0101000000000458144020${publicKey}8685`, 'hex') return addressFromScript(lockupScript) + } else { + return multisigAddressFromPublicKey(publicKey) + } +} + +function multisigAddressFromPublicKey(publicKey: string): string { + if (!isHexString(publicKey)) { + throw new Error(`Invalid public key ${publicKey}, expected a hex-string`) + } + const bytes = Buffer.from(hexToBinUnsafe(publicKey)) + const decodedM = compactSignedIntCodec.decode(bytes) + let index = decodedM.rest.length + 1 + const decodedN = compactSignedIntCodec.decode(bytes.slice(index)) + index += decodedN.rest.length + 1 + const m = compactSignedIntCodec.toI32(decodedM) + const n = compactSignedIntCodec.toI32(decodedN) + if (n <= 0) { + throw new Error(`Invalid n in m-of-n multisig, m: ${m}, n: ${n}`) + } + if (m <= 0 || m > n) { + throw new Error(`Invalid m in m-of-n multisig, m: ${m}, n: ${n}`) + } + if (bytes.length !== PublicKeyBytesLength * n + 2) { + throw new Error('Invalid public key size') + } + + const publicKeyHashes: codec.lockupScript.PublicKeyHash[] = [] + for (; index < bytes.length; index += 33) { + const publicKey = bytes.slice(index, index + 33) + publicKeyHashes.push({ publicKeyHash: Buffer.from(blake.blake2b(publicKey, undefined, 32)) }) + } + const lockupScript: codec.lockupScript.LockupScript = { + scriptType: AddressType.P2MPKH, + script: { + publicKeyHashes: codec.lockupScript.publicKeyHashesCodec.fromArray(publicKeyHashes), + m: compactSignedIntCodec.fromI32(m) + } + } + const encoded = codec.lockupScript.lockupScriptCodec.encode(lockupScript) + return bs58.encode(encoded) +} + +export function encodeMultisigPublicKeys(publicKeys: string[], m: number): string { + if (publicKeys.length === 0) { + throw new Error('Public key array is empty') + } + if (m <= 0 || m > publicKeys.length) { + throw new Error(`Invalid m in m-of-n multisig, m: ${m}, n: ${publicKeys.length}`) } + publicKeys.forEach((publicKey) => { + if (!isHexString(publicKey) || publicKey.length !== PublicKeyBytesLength * 2) { + throw new Error(`Invalid public key: ${publicKey}`) + } + }) + const prefix = Buffer.concat([compactSignedIntCodec.encodeI32(m), compactSignedIntCodec.encodeI32(publicKeys.length)]) + return prefix.toString('hex') + publicKeys.join('') } export function addressFromScript(script: Uint8Array): string { diff --git a/packages/web3/src/utils/exchange.ts b/packages/web3/src/utils/exchange.ts index 58ccb4de3..8e45f2e77 100644 --- a/packages/web3/src/utils/exchange.ts +++ b/packages/web3/src/utils/exchange.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 { AddressType, addressFromPublicKey, addressFromScript, binToHex, bs58, hexToBinUnsafe } from '..' +import { AddressType, addressFromPublicKey, addressFromScript, binToHex, bs58, hexToBinUnsafe } from '../utils' import { Transaction } from '../api/api-alephium' import { Address } from '../signer' 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'