Skip to content

Commit

Permalink
Support explicit groupIndex for groupless address
Browse files Browse the repository at this point in the history
  • Loading branch information
h0ngcha0 committed Feb 13, 2025
1 parent 4e8adbe commit 4a5170d
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 57 deletions.
87 changes: 55 additions & 32 deletions packages/web3/src/address/address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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))
})
Expand Down Expand Up @@ -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', () => {
Expand Down
64 changes: 55 additions & 9 deletions packages/web3/src/address/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -78,24 +77,52 @@ 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
}
}

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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
15 changes: 2 additions & 13 deletions packages/web3/src/contract/ralph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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 {
Expand Down
9 changes: 6 additions & 3 deletions packages/web3/src/signer/tx-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down

0 comments on commit 4a5170d

Please sign in to comment.