diff --git a/packages/web3/src/address/address.test.ts b/packages/web3/src/address/address.test.ts index 2cb2bb2f1..918d54849 100644 --- a/packages/web3/src/address/address.test.ts +++ b/packages/web3/src/address/address.test.ts @@ -65,7 +65,19 @@ describe('address', function () { ).toThrow('Invalid multisig address, n: 2, m: 3') expect(() => validateAddress('thebear')).toThrow('Invalid multisig address') expect(validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK')).toBeUndefined() - expect(() => validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8Bdjfv')).toThrow('Invalid checksum for P2PK address:') + expect(validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@0')).toBeUndefined() + expect(validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@1')).toBeUndefined() + expect(validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@2')).toBeUndefined() + expect(validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@3')).toBeUndefined() + expect(() => validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8Bdjfv')).toThrow( + 'Invalid checksum for P2PK address:' + ) + expect(() => validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@4')).toThrow( + 'Invalid group index: 4' + ) + expect(() => validateAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@j')).toThrow( + 'Invalid group index: j' + ) }) it('should return if an address is valid', () => { @@ -79,6 +91,12 @@ describe('address', function () { expect( isValidAddress('2jW1n2icPtc55Cdm8TF9FjGH681cWthsaZW3gaUFekFZepJoeyY3ZbY7y5SCtAjyCjLL24c4L2Vnfv3KDdAypCddfAY') ).toEqual(true) + expect(isValidAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK')).toEqual(true) + expect(isValidAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@1')).toEqual(true) + expect(isValidAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@2')).toEqual(true) + expect(isValidAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@3')).toEqual(true) + expect(isValidAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@j')).toEqual(false) + expect(isValidAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@4')).toEqual(false) }) it('should get address type', () => { @@ -103,30 +121,35 @@ describe('address', function () { }) it('should calculate the group of addresses', () => { - expect(groupOfAddress('15EM5rGtt7dPRZScE4Z9oL2EDfj84JnoSgq3NNgdcGFyu')).toBe(0), - expect(groupOfAddress('1D59jXR9NpD9ZQqZTRVcVbKVh6ko5TUMt89WvkA8P9P7w')).toBe(1), - expect(groupOfAddress('14tAT3nm7UqVP7gZ35icSdT3AEffv1kaUUMbWQK5PFygr')).toBe(2), - expect(groupOfAddress('12F5aVQoQ7cNrgsVN2YPciwYvwmtJp4ohLa2x4R5KgLbG')).toBe(3), - expect( - groupOfAddress('2jW1n2icPtc55Cdm8TF9FjGH681cWthsaZW3gaUFekFZepJoeyY3ZbY7y5SCtAjyCjLL24c4L2Vnfv3KDdAypCddfAY') - ).toBe(0), - expect( - groupOfAddress('2jXboVD9p66wrAHkPHx2AQocAzYXUWeppmRT3PuVT3ccxX9u8puTnwLeQ2VbTd4sNkgSEgk1cLbyVGLFshGweJCk1Mr') - ).toBe(1), - expect( - groupOfAddress('2je1yvQHpg8bKCDmvr1koELSNbty5DHrHYRkXomiRNvP5VcsZTK3WisBco2sCtCULM2YbxRxPd7QwhdP2hz9PEQwB1S') - ).toBe(2), - expect( - groupOfAddress('2jWukVCejM4Zifz9LvMG4dfR6SEecHLX8VqbswhGwnu61d28B861UhLu3ZmTHu4N14m1kk9rbxreBYzcxta1WPawKzG') - ).toBe(3), - expect(groupOfAddress('eBrjfQNeyUCuxE4zpbfMZcbS3PuvbMJDQBCyk4HRHtX4')).toBe(0), - expect(groupOfAddress('euWxyF55nGTxavL6mgGeMrFdvSRzHor8AmhgPXm8Lm9D')).toBe(1), - expect(groupOfAddress('n2pYTzmA27tkp7UNFPhMJpjz3jr5vgessxqJ7kwomBMF')).toBe(2), - expect(groupOfAddress('tLf6hDfrUugmxZhKxGoZMpAUBt3NcZ2hrTspTCmZ6JdQ')).toBe(3), - expect(groupOfAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9kj')).toBe(0), - expect(groupOfAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9kk')).toBe(1), - expect(groupOfAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9km')).toBe(2), - expect(groupOfAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9kn')).toBe(3) + expect(groupOfAddress('15EM5rGtt7dPRZScE4Z9oL2EDfj84JnoSgq3NNgdcGFyu')).toBe(0) + expect(groupOfAddress('1D59jXR9NpD9ZQqZTRVcVbKVh6ko5TUMt89WvkA8P9P7w')).toBe(1) + expect(groupOfAddress('14tAT3nm7UqVP7gZ35icSdT3AEffv1kaUUMbWQK5PFygr')).toBe(2) + expect(groupOfAddress('12F5aVQoQ7cNrgsVN2YPciwYvwmtJp4ohLa2x4R5KgLbG')).toBe(3) + expect( + groupOfAddress('2jW1n2icPtc55Cdm8TF9FjGH681cWthsaZW3gaUFekFZepJoeyY3ZbY7y5SCtAjyCjLL24c4L2Vnfv3KDdAypCddfAY') + ).toBe(0) + expect( + groupOfAddress('2jXboVD9p66wrAHkPHx2AQocAzYXUWeppmRT3PuVT3ccxX9u8puTnwLeQ2VbTd4sNkgSEgk1cLbyVGLFshGweJCk1Mr') + ).toBe(1) + expect( + groupOfAddress('2je1yvQHpg8bKCDmvr1koELSNbty5DHrHYRkXomiRNvP5VcsZTK3WisBco2sCtCULM2YbxRxPd7QwhdP2hz9PEQwB1S') + ).toBe(2) + expect( + groupOfAddress('2jWukVCejM4Zifz9LvMG4dfR6SEecHLX8VqbswhGwnu61d28B861UhLu3ZmTHu4N14m1kk9rbxreBYzcxta1WPawKzG') + ).toBe(3) + expect(groupOfAddress('eBrjfQNeyUCuxE4zpbfMZcbS3PuvbMJDQBCyk4HRHtX4')).toBe(0) + expect(groupOfAddress('euWxyF55nGTxavL6mgGeMrFdvSRzHor8AmhgPXm8Lm9D')).toBe(1) + expect(groupOfAddress('n2pYTzmA27tkp7UNFPhMJpjz3jr5vgessxqJ7kwomBMF')).toBe(2) + expect(groupOfAddress('tLf6hDfrUugmxZhKxGoZMpAUBt3NcZ2hrTspTCmZ6JdQ')).toBe(3) + expect(groupOfAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9kj')).toBe(0) + expect(groupOfAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9kk')).toBe(1) + expect(groupOfAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9km')).toBe(2) + expect(groupOfAddress('yya86C6UemCeLs5Ztwjcf2Mp2Kkt4mwzzRpBiG6qQ9kn')).toBe(3) + expect(groupOfAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK')).toBe(2) + expect(groupOfAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@0')).toBe(0) + expect(groupOfAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@1')).toBe(1) + expect(groupOfAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@2')).toBe(2) + expect(groupOfAddress('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK@3')).toBe(3) }) it('should calculate the group of lockup script', () => { @@ -154,7 +177,7 @@ describe('address', function () { const publicKeyLike = new Uint8Array([0x00, ...bytes4]) const checkSum = intAs4BytesCodec.encode(djb2(publicKeyLike)) const scriptHint = djb2(bytes4) | 1 - const p2pk: LockupScript = { kind: 'P2PK', value: { type: 0, publicKey: bytes4, scriptHint, checkSum} } + const p2pk: LockupScript = { kind: 'P2PK', value: { type: 0, publicKey: bytes4, scriptHint, checkSum } } const p2pkAddress = bs58.encode(new Uint8Array([0x04, 0x00, ...bytes4, ...checkSum])) expect(groupOfAddress(p2pkAddress)).toBe(groupOfLockupScript(p2pk)) }) @@ -192,12 +215,12 @@ describe('address', function () { expect(addressFromPublicKey('030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc')).toBe( '1ACCkgFfmTif46T3qK12znuWjb5Bk9jXpqaeWt2DXx8oc' ) - expect(addressFromPublicKey('029592852f5d289785904b89a073ff80ee6155c894b1d13ecb16bcf3ac02473e1a', 'groupless')).toBe( - '3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK' - ) - expect(addressFromPublicKey('aecfc38a48f5fe7e050fca59de9f8d77fa7a7d9e63af608a95f8839de397f48a', 'bip340-schnorr')).toBe( - 'qvegNNcKFBtkMcZTLj42pki2YDYTvHaGyBxBaWrPaHwj' - ) + expect( + addressFromPublicKey('029592852f5d289785904b89a073ff80ee6155c894b1d13ecb16bcf3ac02473e1a', 'groupless') + ).toBe('3cUqhqEgt8qFAokkD7qRsy9Q2Q9S1LEiSdogbBmaq7CnshB8BdjfK') + expect( + addressFromPublicKey('aecfc38a48f5fe7e050fca59de9f8d77fa7a7d9e63af608a95f8839de397f48a', 'bip340-schnorr') + ).toBe('qvegNNcKFBtkMcZTLj42pki2YDYTvHaGyBxBaWrPaHwj') }) it('should convert between contract id and address', () => { diff --git a/packages/web3/src/address/address.ts b/packages/web3/src/address/address.ts index 1ed083a17..d3e02f033 100644 --- a/packages/web3/src/address/address.ts +++ b/packages/web3/src/address/address.ts @@ -54,8 +54,7 @@ export function isValidAddress(address: string): boolean { } function decodeAndValidateAddress(address: string): Uint8Array { - // TODO: Need to consider groupless address with @group - const decoded = base58ToBytes(address) + const decoded = addressToBytes(address) if (decoded.length === 0) throw new Error('Address is empty') const addressType = decoded[0] if (addressType === AddressType.P2MPKH) { @@ -78,14 +77,15 @@ function decodeAndValidateAddress(address: string): Uint8Array { // [type, ...hash] if (decoded.length === 33) return decoded } else if (addressType === AddressType.P2PK) { - // [type, keyType, ...publicKey, ...checkSum] - if (decoded.length === 39) { - const publicKeyLikeBytes = decoded.slice(1, 35) // Include the keyType byte + if (decoded.length === 43) { + // [type, keyType, ...publicKey, ...checkSum, ...scriptHint] + const publicKeyLikeBytes = decoded.slice(1, 35) const checksum = binToHex(decoded.slice(35, 39)) const expectedChecksum = binToHex(intAs4BytesCodec.encode(djb2(publicKeyLikeBytes))) if (checksum !== expectedChecksum) { throw new Error(`Invalid checksum for P2PK address: ${address}`) } + return decoded } } @@ -93,9 +93,36 @@ function decodeAndValidateAddress(address: string): Uint8Array { throw new Error(`Invalid address: ${address}`) } +export function addressToBytes(address: string): Uint8Array { + if (address.length > 2 && address[address.length - 2] === '@') { + const groupIndex = parseGroupIndex(address[address.length - 1]) + const decoded = base58ToBytes(address.slice(0, address.length - 2)) + if (decoded[0] === 0x04 && decoded.length === 39) { + const publicKeyBytes = decoded.slice(2, 35) + const scriptHint = findScriptHint(djb2(publicKeyBytes) | 1, groupIndex) + const scriptHintBytes = intAs4BytesCodec.encode(scriptHint) + return new Uint8Array([...decoded, ...scriptHintBytes]) + } + throw new Error(`Invalid groupless address: ${address}`) + } else { + const decoded = base58ToBytes(address) + if (decoded[0] === 0x04 && decoded.length === 39) { + const publicKeyBytes = decoded.slice(2, 35) + const scriptHintBytes = intAs4BytesCodec.encode(djb2(publicKeyBytes) | 1) + return new Uint8Array([...decoded, ...scriptHintBytes]) + } + return decoded + } +} + export function isAssetAddress(address: string) { const addressType = decodeAndValidateAddress(address)[0] - return addressType === AddressType.P2PKH || addressType === AddressType.P2MPKH || addressType === AddressType.P2SH || addressType === AddressType.P2PK + return ( + addressType === AddressType.P2PKH || + addressType === AddressType.P2MPKH || + addressType === AddressType.P2SH || + addressType === AddressType.P2PK + ) } export function isGrouplessAddress(address: string) { @@ -139,7 +166,7 @@ function groupOfP2mpkhAddress(address: Uint8Array): number { } function groupOfP2pkAddress(address: Uint8Array): number { - return groupFromBytes(address.slice(1, 34)) + return groupFromHint(intAs4BytesCodec.decode(address.slice(38, 42))) } // Pay to script hash address @@ -250,8 +277,7 @@ export function groupOfLockupScript(lockupScript: LockupScript): number { } else if (lockupScript.kind === 'P2SH') { return groupFromBytes(lockupScript.value) } else if (lockupScript.kind === 'P2PK') { - // FIXME: support @group for groupless address - return groupFromBytes(lockupScript.value.publicKey) + return groupFromHint(lockupScript.value.scriptHint) } else { // P2C const contractId = lockupScript.value @@ -261,6 +287,26 @@ export function groupOfLockupScript(lockupScript: LockupScript): number { export function groupFromBytes(bytes: Uint8Array): number { const hint = djb2(bytes) | 1 + return groupFromHint(hint) +} + +export function groupFromHint(hint: number): number { const hash = xorByte(hint) return hash % TOTAL_NUMBER_OF_GROUPS } + +function findScriptHint(hint: number, groupIndex: number): number { + if (groupFromHint(hint) === groupIndex) { + return hint + } else { + return findScriptHint(hint + 1, groupIndex) + } +} + +function parseGroupIndex(groupIndexStr: string): number { + const groupIndex = parseInt(groupIndexStr) + if (isNaN(groupIndex) || groupIndex < 0 || groupIndex >= TOTAL_NUMBER_OF_GROUPS) { + throw new Error(`Invalid group index: ${groupIndexStr}`) + } + return groupIndex +} diff --git a/packages/web3/src/contract/ralph.ts b/packages/web3/src/contract/ralph.ts index 2f51e9f16..7369df919 100644 --- a/packages/web3/src/contract/ralph.ts +++ b/packages/web3/src/contract/ralph.ts @@ -36,6 +36,7 @@ import { import { boolCodec } from '../codec/codec' import { TraceableError } from '../error' import djb2 from '../utils/djb2' +import { addressToBytes, groupFromHint } from '../address' export function encodeByteVec(hex: string): Uint8Array { if (!isHexString(hex)) { @@ -47,19 +48,7 @@ export function encodeByteVec(hex: string): Uint8Array { } export function encodeAddress(address: string): Uint8Array { - // Check if address has group suffix (e.g. "@0") - if (address.length > 2 && address[address.length - 2] === '@') { - throw new Error("TODO: support groupless address with @group") - } else { - const decoded = bs58.decode(address) - if (decoded[0] === 0x04 && decoded.length === 39) { - // If it is groupless address without @group, we need to add script hint - const publicKeyBytes = decoded.slice(2, 35) - const scriptHintBytes = intAs4BytesCodec.encode(djb2(publicKeyBytes) | 1) - return new Uint8Array([...decoded, ...scriptHintBytes]) - } - return decoded - } + return addressToBytes(address) } export enum VmValType { diff --git a/packages/web3/src/signer/tx-builder.ts b/packages/web3/src/signer/tx-builder.ts index ea6fce58e..6d649cb2e 100644 --- a/packages/web3/src/signer/tx-builder.ts +++ b/packages/web3/src/signer/tx-builder.ts @@ -240,7 +240,9 @@ export abstract class TransactionBuilder { } } - private buildGrouplessDeployContractTxParams(params: SignGrouplessDeployContractTxParams): node.BuildGrouplessDeployContractTx { + private buildGrouplessDeployContractTxParams( + params: SignGrouplessDeployContractTxParams + ): node.BuildGrouplessDeployContractTx { return { fromAddress: params.fromAddress, bytecode: params.bytecode, @@ -253,8 +255,9 @@ export abstract class TransactionBuilder { } } - - private buildGrouplessExecuteScriptTxParams(params: SignGrouplessExecuteScriptTxParams): node.BuildGrouplessExecuteScriptTx { + private buildGrouplessExecuteScriptTxParams( + params: SignGrouplessExecuteScriptTxParams + ): node.BuildGrouplessExecuteScriptTx { return { fromAddress: params.fromAddress, bytecode: params.bytecode,