|
| 1 | +import { addHexPrefix, toBuffer } from 'ethereumjs-util'; |
| 2 | +import EthereumAbi from 'ethereumjs-abi'; |
| 3 | + |
| 4 | +// --------------------------------------------------------------------------- |
| 5 | +// Constants |
| 6 | +// --------------------------------------------------------------------------- |
| 7 | + |
| 8 | +// ABI parameter type arrays |
| 9 | +export const delegateForUserDecryptionTypes = ['address', 'address', 'uint64'] as const; |
| 10 | +export const callFromParentTypes = ['address', 'uint256', 'bytes'] as const; |
| 11 | +export const aclMulticallTypes = ['bytes[]'] as const; |
| 12 | + |
| 13 | +/** |
| 14 | + * Function selector for ACL.delegateForUserDecryption(address,address,uint64) |
| 15 | + * = keccak256('delegateForUserDecryption(address,address,uint64)')[0:4] |
| 16 | + */ |
| 17 | +export const delegateForUserDecryptionMethodId = addHexPrefix( |
| 18 | + EthereumAbi.methodID('delegateForUserDecryption', [...delegateForUserDecryptionTypes]).toString('hex') |
| 19 | +); |
| 20 | + |
| 21 | +/** |
| 22 | + * Function selector for ACL.multicall(bytes[]) |
| 23 | + * = keccak256('multicall(bytes[])')[0:4] |
| 24 | + * ACL inherits OpenZeppelin MulticallUpgradeable — preserves msg.sender via delegatecall. |
| 25 | + */ |
| 26 | +export const aclMulticallMethodId = addHexPrefix( |
| 27 | + EthereumAbi.methodID('multicall', [...aclMulticallTypes]).toString('hex') |
| 28 | +); |
| 29 | + |
| 30 | +/** |
| 31 | + * Function selector for ForwarderV4.callFromParent(address,uint256,bytes) |
| 32 | + * = keccak256('callFromParent(address,uint256,bytes)')[0:4] |
| 33 | + */ |
| 34 | +export const callFromParentMethodId = addHexPrefix( |
| 35 | + EthereumAbi.methodID('callFromParent', [...callFromParentTypes]).toString('hex') |
| 36 | +); |
| 37 | + |
| 38 | +// --------------------------------------------------------------------------- |
| 39 | +// Encoding functions |
| 40 | +// --------------------------------------------------------------------------- |
| 41 | + |
| 42 | +/** |
| 43 | + * Encodes a single ACL.delegateForUserDecryption() call. |
| 44 | + * |
| 45 | + * Grants `delegateAddress` the right to decrypt ERC-7984 token balances on |
| 46 | + * behalf of the calling address (msg.sender) for the specified token contract. |
| 47 | + * |
| 48 | + * @param delegateAddress BitGo enterprise viewing key address |
| 49 | + * @param tokenContractAddress ERC-7984 token contract address |
| 50 | + * @param expiryTimestamp Unix seconds; recommended: Math.floor(Date.now()/1000) + 365*86400 |
| 51 | + * @returns ABI-encoded calldata hex string (0x-prefixed) |
| 52 | + */ |
| 53 | +export function buildDelegationCalldata( |
| 54 | + delegateAddress: string, |
| 55 | + tokenContractAddress: string, |
| 56 | + expiryTimestamp: number |
| 57 | +): string { |
| 58 | + const method = EthereumAbi.methodID('delegateForUserDecryption', [...delegateForUserDecryptionTypes]); |
| 59 | + const args = EthereumAbi.rawEncode( |
| 60 | + [...delegateForUserDecryptionTypes], |
| 61 | + [delegateAddress, tokenContractAddress, expiryTimestamp] |
| 62 | + ); |
| 63 | + return addHexPrefix(Buffer.concat([method, args]).toString('hex')); |
| 64 | +} |
| 65 | + |
| 66 | +/** |
| 67 | + * Encodes N delegateForUserDecryption calls batched inside ACL.multicall(). |
| 68 | + * |
| 69 | + * Produces a single TX that grants delegation for all specified token contracts. |
| 70 | + * Requires tokenContractAddresses.length >= 1. |
| 71 | + * Note: DecryptionDelegationBuilder always uses this function (even for a single token) |
| 72 | + * to keep the transaction shape consistent regardless of token count. |
| 73 | + * |
| 74 | + * @param delegateAddress BitGo enterprise viewing key address |
| 75 | + * @param tokenContractAddresses Array of ERC-7984 token contract addresses |
| 76 | + * @param expiryTimestamp Unix seconds |
| 77 | + * @returns ABI-encoded calldata hex string (0x-prefixed) |
| 78 | + */ |
| 79 | +export function buildMulticallDelegationCalldata( |
| 80 | + delegateAddress: string, |
| 81 | + tokenContractAddresses: string[], |
| 82 | + expiryTimestamp: number |
| 83 | +): string { |
| 84 | + if (tokenContractAddresses.length === 0) { |
| 85 | + throw new Error('buildMulticallDelegationCalldata: tokenContractAddresses must not be empty'); |
| 86 | + } |
| 87 | + |
| 88 | + // Build each inner delegateForUserDecryption call as raw bytes |
| 89 | + const innerCalls: Buffer[] = tokenContractAddresses.map((tokenAddress) => { |
| 90 | + const innerMethod = EthereumAbi.methodID('delegateForUserDecryption', [...delegateForUserDecryptionTypes]); |
| 91 | + const innerArgs = EthereumAbi.rawEncode( |
| 92 | + [...delegateForUserDecryptionTypes], |
| 93 | + [delegateAddress, tokenAddress, expiryTimestamp] |
| 94 | + ); |
| 95 | + return Buffer.concat([innerMethod, innerArgs]); |
| 96 | + }); |
| 97 | + |
| 98 | + // Encode outer multicall(bytes[]) |
| 99 | + const outerMethod = EthereumAbi.methodID('multicall', [...aclMulticallTypes]); |
| 100 | + const outerArgs = EthereumAbi.rawEncode([...aclMulticallTypes], [innerCalls]); |
| 101 | + return addHexPrefix(Buffer.concat([outerMethod, outerArgs]).toString('hex')); |
| 102 | +} |
| 103 | + |
| 104 | +/** |
| 105 | + * Wraps calldata in a ForwarderV4.callFromParent(target, 0, data) call. |
| 106 | + * |
| 107 | + * Used when a forwarder contract must be msg.sender for an external contract |
| 108 | + * call — for example, when the forwarder itself needs to call |
| 109 | + * ACL.delegateForUserDecryption() so that its own balance can be decrypted. |
| 110 | + * |
| 111 | + * Only the parentAddress (root wallet) is allowed to call callFromParent |
| 112 | + * (enforced by the forwarder's onlyParent modifier). |
| 113 | + * |
| 114 | + * @param targetAddress Address of the contract the forwarder will call (e.g. ACL) |
| 115 | + * @param calldata ABI-encoded inner calldata (e.g. from buildDelegationCalldata) |
| 116 | + * @returns ABI-encoded callFromParent calldata hex string (0x-prefixed) |
| 117 | + */ |
| 118 | +export function wrapInCallFromParent(targetAddress: string, calldata: string): string { |
| 119 | + const method = EthereumAbi.methodID('callFromParent', [...callFromParentTypes]); |
| 120 | + const args = EthereumAbi.rawEncode( |
| 121 | + [...callFromParentTypes], |
| 122 | + [ |
| 123 | + targetAddress, |
| 124 | + 0, // value: no ETH transfer |
| 125 | + toBuffer(calldata), // inner calldata as bytes |
| 126 | + ] |
| 127 | + ); |
| 128 | + return addHexPrefix(Buffer.concat([method, args]).toString('hex')); |
| 129 | +} |
0 commit comments