@aboutcircles/sdk-utils 0.1.25 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/abi.d.ts +7 -0
- package/dist/abi.d.ts.map +1 -1
- package/dist/abi.js +18 -1
- package/dist/contractErrors.d.ts +1 -1
- package/dist/contractErrors.d.ts.map +1 -1
- package/dist/contractErrors.js +2 -2
- package/dist/crypto.js +1 -1
- package/dist/index.d.ts +13 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -10
- package/dist/tests/abi.test.js +1 -1
- package/dist/tests/checksumAddress.test.js +1 -1
- package/dist/tests/transferData.test.d.ts +2 -0
- package/dist/tests/transferData.test.d.ts.map +1 -0
- package/dist/tests/transferData.test.js +243 -0
- package/dist/transferData.d.ts +30 -0
- package/dist/transferData.d.ts.map +1 -0
- package/dist/transferData.js +314 -0
- package/package.json +1 -1
package/dist/abi.d.ts
CHANGED
|
@@ -52,4 +52,11 @@ export declare function decodeErrorResult(config: {
|
|
|
52
52
|
* ```
|
|
53
53
|
*/
|
|
54
54
|
export declare function encodeAbiParameters(types: string[], values: unknown[]): Hex;
|
|
55
|
+
/**
|
|
56
|
+
* Decode ABI-encoded parameters given their type strings.
|
|
57
|
+
* @param types - Array of Solidity type strings (e.g. ['address', 'uint256'])
|
|
58
|
+
* @param data - Hex-encoded data (with or without 0x prefix), without selector
|
|
59
|
+
* @returns Array of decoded values
|
|
60
|
+
*/
|
|
61
|
+
export declare function decodeAbiParameters(types: string[], data: string): unknown[];
|
|
55
62
|
//# sourceMappingURL=abi.d.ts.map
|
package/dist/abi.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abi.d.ts","sourceRoot":"","sources":["../src/abi.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAe,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAwCnD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASvD;AAoXD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,GAAG,EAAE,GAAG,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC3B,GAAG,GAAG,CA+DN;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,GAAG,EAAE,GAAG,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CA0CV;AAmBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,GAAG,EAAE,GAAG,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;CACd,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,GAAG,IAAI,CA+C7C;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CA+C3E"}
|
|
1
|
+
{"version":3,"file":"abi.d.ts","sourceRoot":"","sources":["../src/abi.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAe,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAwCnD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CASvD;AAoXD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,GAAG,EAAE,GAAG,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC3B,GAAG,GAAG,CA+DN;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,GAAG,EAAE,GAAG,CAAC;IACT,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CA0CV;AAmBD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,GAAG,EAAE,GAAG,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;CACd,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,GAAG,IAAI,CA+C7C;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CA+C3E;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAY5E"}
|
package/dist/abi.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { keccak_256 } from '@noble/hashes/sha3.js';
|
|
2
|
-
import { bytesToHex } from './bytes';
|
|
2
|
+
import { bytesToHex } from './bytes.js';
|
|
3
3
|
// ============================================================================
|
|
4
4
|
// CONSTANTS & UTILITIES
|
|
5
5
|
// ============================================================================
|
|
@@ -511,3 +511,20 @@ export function encodeAbiParameters(types, values) {
|
|
|
511
511
|
}
|
|
512
512
|
return ('0x' + result + dynamicData.join(''));
|
|
513
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Decode ABI-encoded parameters given their type strings.
|
|
516
|
+
* @param types - Array of Solidity type strings (e.g. ['address', 'uint256'])
|
|
517
|
+
* @param data - Hex-encoded data (with or without 0x prefix), without selector
|
|
518
|
+
* @returns Array of decoded values
|
|
519
|
+
*/
|
|
520
|
+
export function decodeAbiParameters(types, data) {
|
|
521
|
+
const cleanData = data.startsWith('0x') ? data.slice(2) : data;
|
|
522
|
+
const results = [];
|
|
523
|
+
let offset = 0;
|
|
524
|
+
for (const type of types) {
|
|
525
|
+
const result = decodeParam(type, cleanData, offset);
|
|
526
|
+
results.push(result.value);
|
|
527
|
+
offset += result.consumed;
|
|
528
|
+
}
|
|
529
|
+
return results;
|
|
530
|
+
}
|
package/dist/contractErrors.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contractErrors.d.ts","sourceRoot":"","sources":["../src/contractErrors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAEpE,OAAO,EAAE,YAAY,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"contractErrors.d.ts","sourceRoot":"","sources":["../src/contractErrors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAEpE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA6H3C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,GAAG,EACV,GAAG,EAAE,GAAG,GACP,oBAAoB,GAAG,IAAI,CA4B7B;AAED;;;GAGG;AACH,qBAAa,aAAc,SAAQ,YAAY,CAAC,MAAM,CAAC;IACrD,SAAgB,YAAY,CAAC,EAAE,oBAAoB,CAAC;gBAGlD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC9B,YAAY,CAAC,EAAE,oBAAoB,CAAC;KACrC;IAMH;;OAEG;IACH,MAAM,CAAC,oBAAoB,CACzB,KAAK,EAAE,GAAG,EACV,GAAG,EAAE,GAAG,EACR,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,aAAa;IA8BhB;;OAEG;IACH,QAAQ,IAAI,MAAM;CAcnB"}
|
package/dist/contractErrors.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Contract Error Parsing
|
|
3
3
|
* Utilities for parsing and decoding contract errors from transaction failures
|
|
4
4
|
*/
|
|
5
|
-
import { decodeErrorResult } from './abi';
|
|
6
|
-
import { CirclesError } from './errors';
|
|
5
|
+
import { decodeErrorResult } from './abi.js';
|
|
6
|
+
import { CirclesError } from './errors.js';
|
|
7
7
|
/**
|
|
8
8
|
* Contract error codes mapping
|
|
9
9
|
* Maps error codes to human-readable messages
|
package/dist/crypto.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
2
2
|
import { keccak_256 } from '@noble/hashes/sha3';
|
|
3
|
-
import { checksumAddress } from './abi';
|
|
3
|
+
import { checksumAddress } from './abi.js';
|
|
4
4
|
/**
|
|
5
5
|
* Generate a random private key (32 bytes / 64 hex chars)
|
|
6
6
|
* @returns Private key as hex string with 0x prefix
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
export { CirclesConverter } from './circlesConverter';
|
|
2
|
-
export { bytesToHex, hexToBytes } from './bytes';
|
|
3
|
-
export { encodeFunctionData, decodeFunctionResult, decodeErrorResult, checksumAddress, encodeAbiParameters } from './abi';
|
|
4
|
-
export { cidV0ToHex, cidV0ToUint8Array } from './cid';
|
|
5
|
-
export { uint256ToAddress } from './address';
|
|
6
|
-
export { ZERO_ADDRESS, INVITATION_FEE, MAX_FLOW, SAFE_PROXY_FACTORY, ACCOUNT_INITIALIZER_HASH, ACCOUNT_CREATION_CODE_HASH, GNOSIS_GROUP_ADDRESS, FARM_DESTINATION } from './constants';
|
|
7
|
-
export { circlesConfig } from './config';
|
|
8
|
-
export { parseContractError, ContractError } from './contractErrors';
|
|
9
|
-
export { generatePrivateKey, privateKeyToAddress, keccak256 } from './crypto';
|
|
10
|
-
export {
|
|
11
|
-
export type {
|
|
1
|
+
export { CirclesConverter } from './circlesConverter.js';
|
|
2
|
+
export { bytesToHex, hexToBytes } from './bytes.js';
|
|
3
|
+
export { encodeFunctionData, decodeFunctionResult, decodeErrorResult, checksumAddress, encodeAbiParameters, decodeAbiParameters } from './abi.js';
|
|
4
|
+
export { cidV0ToHex, cidV0ToUint8Array } from './cid.js';
|
|
5
|
+
export { uint256ToAddress } from './address.js';
|
|
6
|
+
export { ZERO_ADDRESS, INVITATION_FEE, MAX_FLOW, SAFE_PROXY_FACTORY, ACCOUNT_INITIALIZER_HASH, ACCOUNT_CREATION_CODE_HASH, GNOSIS_GROUP_ADDRESS, FARM_DESTINATION } from './constants.js';
|
|
7
|
+
export { circlesConfig } from './config.js';
|
|
8
|
+
export { parseContractError, ContractError } from './contractErrors.js';
|
|
9
|
+
export { generatePrivateKey, privateKeyToAddress, keccak256 } from './crypto.js';
|
|
10
|
+
export { encodeCrcV2TransferData, decodeCrcV2TransferData } from './transferData.js';
|
|
11
|
+
export type { TransferDataType, DecodedTransferData, DecodedAbiPayload, DecodedMetadataPayload } from './transferData.js';
|
|
12
|
+
export { CirclesError, ValidationError, EncodingError, wrapError, isCirclesError, getErrorMessage, } from './errors.js';
|
|
13
|
+
export type { BaseErrorSource, UtilsErrorSource } from './errors.js';
|
|
12
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,eAAe,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAClJ,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC1L,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACrF,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAG1H,OAAO,EACL,YAAY,EACZ,eAAe,EACf,aAAa,EACb,SAAS,EACT,cAAc,EACd,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
export { CirclesConverter } from './circlesConverter';
|
|
2
|
-
export { bytesToHex, hexToBytes } from './bytes';
|
|
3
|
-
export { encodeFunctionData, decodeFunctionResult, decodeErrorResult, checksumAddress, encodeAbiParameters } from './abi';
|
|
4
|
-
export { cidV0ToHex, cidV0ToUint8Array } from './cid';
|
|
5
|
-
export { uint256ToAddress } from './address';
|
|
6
|
-
export { ZERO_ADDRESS, INVITATION_FEE, MAX_FLOW, SAFE_PROXY_FACTORY, ACCOUNT_INITIALIZER_HASH, ACCOUNT_CREATION_CODE_HASH, GNOSIS_GROUP_ADDRESS, FARM_DESTINATION } from './constants';
|
|
7
|
-
export { circlesConfig } from './config';
|
|
8
|
-
export { parseContractError, ContractError } from './contractErrors';
|
|
9
|
-
export { generatePrivateKey, privateKeyToAddress, keccak256 } from './crypto';
|
|
1
|
+
export { CirclesConverter } from './circlesConverter.js';
|
|
2
|
+
export { bytesToHex, hexToBytes } from './bytes.js';
|
|
3
|
+
export { encodeFunctionData, decodeFunctionResult, decodeErrorResult, checksumAddress, encodeAbiParameters, decodeAbiParameters } from './abi.js';
|
|
4
|
+
export { cidV0ToHex, cidV0ToUint8Array } from './cid.js';
|
|
5
|
+
export { uint256ToAddress } from './address.js';
|
|
6
|
+
export { ZERO_ADDRESS, INVITATION_FEE, MAX_FLOW, SAFE_PROXY_FACTORY, ACCOUNT_INITIALIZER_HASH, ACCOUNT_CREATION_CODE_HASH, GNOSIS_GROUP_ADDRESS, FARM_DESTINATION } from './constants.js';
|
|
7
|
+
export { circlesConfig } from './config.js';
|
|
8
|
+
export { parseContractError, ContractError } from './contractErrors.js';
|
|
9
|
+
export { generatePrivateKey, privateKeyToAddress, keccak256 } from './crypto.js';
|
|
10
|
+
export { encodeCrcV2TransferData, decodeCrcV2TransferData } from './transferData.js';
|
|
10
11
|
// Error handling
|
|
11
|
-
export { CirclesError, ValidationError, EncodingError, wrapError, isCirclesError, getErrorMessage, } from './errors';
|
|
12
|
+
export { CirclesError, ValidationError, EncodingError, wrapError, isCirclesError, getErrorMessage, } from './errors.js';
|
package/dist/tests/abi.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { encodeFunctionData, decodeFunctionResult } from '../abi';
|
|
2
|
+
import { encodeFunctionData, decodeFunctionResult } from '../abi.js';
|
|
3
3
|
import { encodeFunctionData as viemEncodeFunctionData, decodeFunctionResult as viemDecodeFunctionResult } from 'viem/utils';
|
|
4
4
|
describe('encodeFunctionData', () => {
|
|
5
5
|
test('encodes function with no inputs', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
import { checksumAddress } from '../abi';
|
|
2
|
+
import { checksumAddress } from '../abi.js';
|
|
3
3
|
describe('checksumAddress', () => {
|
|
4
4
|
test('checksums lowercase address correctly', () => {
|
|
5
5
|
const lowercase = '0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transferData.test.d.ts","sourceRoot":"","sources":["../../src/tests/transferData.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { encodeCrcV2TransferData, decodeCrcV2TransferData } from '../transferData.js';
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Type 0x0001 — UTF-8 Text
|
|
5
|
+
// ============================================================================
|
|
6
|
+
describe('Type 0x0001 — UTF-8 text', () => {
|
|
7
|
+
test('encodes "Hi"', () => {
|
|
8
|
+
const result = encodeCrcV2TransferData(['Hi'], 0x0001);
|
|
9
|
+
expect(result).toBe('0x010001000248' + '69');
|
|
10
|
+
});
|
|
11
|
+
test('encodes "Happy Birthday 🎂"', () => {
|
|
12
|
+
const result = encodeCrcV2TransferData(['Happy Birthday 🎂'], 0x0001);
|
|
13
|
+
expect(result).toBe('0x0100010013486170707920426972746864617920F09F8E82'.toLowerCase());
|
|
14
|
+
});
|
|
15
|
+
test('round-trips simple text', () => {
|
|
16
|
+
const text = 'Thanks for the lunch! 🍣';
|
|
17
|
+
const encoded = encodeCrcV2TransferData([text], 0x0001);
|
|
18
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
19
|
+
expect(decoded.type).toBe(0x0001);
|
|
20
|
+
expect(decoded.payload).toBe(text);
|
|
21
|
+
expect(decoded.length).toBe(new TextEncoder().encode(text).length);
|
|
22
|
+
});
|
|
23
|
+
test('round-trips empty string', () => {
|
|
24
|
+
const encoded = encodeCrcV2TransferData([''], 0x0001);
|
|
25
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
26
|
+
expect(decoded.payload).toBe('');
|
|
27
|
+
expect(decoded.length).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
test('round-trips multi-line text', () => {
|
|
30
|
+
const text = 'Line 1\nLine 2\nLine 3';
|
|
31
|
+
const encoded = encodeCrcV2TransferData([text], 0x0001);
|
|
32
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
33
|
+
expect(decoded.payload).toBe(text);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Type 0x0002 — Raw 32-byte hex (XMTP message ID)
|
|
38
|
+
// ============================================================================
|
|
39
|
+
describe('Type 0x0002 — Raw 32-byte hex', () => {
|
|
40
|
+
const xmtpId = '0x69236a76eff7f05583e6fa5810cf6b24100f0e9132e0787594a790b2fea254ec';
|
|
41
|
+
test('encodes XMTP message ID', () => {
|
|
42
|
+
const result = encodeCrcV2TransferData([xmtpId], 0x0002);
|
|
43
|
+
expect(result).toBe('0x010002002069236a76eff7f05583e6fa5810cf6b24100f0e9132e0787594a790b2fea254ec');
|
|
44
|
+
});
|
|
45
|
+
test('round-trips XMTP message ID', () => {
|
|
46
|
+
const encoded = encodeCrcV2TransferData([xmtpId], 0x0002);
|
|
47
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
48
|
+
expect(decoded.type).toBe(0x0002);
|
|
49
|
+
expect(decoded.length).toBe(32);
|
|
50
|
+
expect(decoded.payload.toLowerCase()).toBe(xmtpId.toLowerCase());
|
|
51
|
+
});
|
|
52
|
+
test('accepts hex without 0x prefix', () => {
|
|
53
|
+
const raw = '69236a76eff7f05583e6fa5810cf6b24100f0e9132e0787594a790b2fea254ec';
|
|
54
|
+
const encoded = encodeCrcV2TransferData([raw], 0x0002);
|
|
55
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
56
|
+
expect(decoded.payload.slice(2)).toBe(raw);
|
|
57
|
+
});
|
|
58
|
+
test('throws for wrong byte length', () => {
|
|
59
|
+
expect(() => encodeCrcV2TransferData(['0xdeadbeef'], 0x0002)).toThrow();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Type 0x0003 — IPFS CID
|
|
64
|
+
// ============================================================================
|
|
65
|
+
describe('Type 0x0003 — IPFS CID', () => {
|
|
66
|
+
const cidV0 = 'QmZ4tDuvesekSs4qM5ZBKpXiZGun7S2CYtEZRB3DYXkjGx';
|
|
67
|
+
test('encodes CIDv0', () => {
|
|
68
|
+
const result = encodeCrcV2TransferData([cidV0], 0x0003);
|
|
69
|
+
expect(result).toMatch(/^0x01000300/);
|
|
70
|
+
});
|
|
71
|
+
test('round-trips CIDv0 back to CIDv0 string (dag-pb + sha256)', () => {
|
|
72
|
+
const encoded = encodeCrcV2TransferData([cidV0], 0x0003);
|
|
73
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
74
|
+
expect(decoded.type).toBe(0x0003);
|
|
75
|
+
expect(decoded.payload).toBe(cidV0);
|
|
76
|
+
});
|
|
77
|
+
test('round-trips CIDv1 string', () => {
|
|
78
|
+
// CIDv1 base32 (dag-pb + sha256, same content as CIDv0 above)
|
|
79
|
+
const cidV1 = 'bafybeie7m2fsbt6sjtn7tymyb6sim7iiyz6szl4ethtn7anzx4frzfzipu';
|
|
80
|
+
const encoded = encodeCrcV2TransferData([cidV1], 0x0003);
|
|
81
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
82
|
+
expect(decoded.type).toBe(0x0003);
|
|
83
|
+
// dag-pb + sha256 CIDv1 should decode back to CIDv0
|
|
84
|
+
expect(decoded.payload).toBe(cidV0);
|
|
85
|
+
});
|
|
86
|
+
test('encodes and decodes produce consistent length', () => {
|
|
87
|
+
const encoded = encodeCrcV2TransferData([cidV0], 0x0003);
|
|
88
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
89
|
+
// CIDv1 hex for dag-pb sha256 = 36 bytes (1 version + 1 codec + 2 multihash header + 32 digest)
|
|
90
|
+
expect(decoded.length).toBe(36);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Type 0x1001 — UTF-8 text with metadata
|
|
95
|
+
// ============================================================================
|
|
96
|
+
describe('Type 0x1001 — UTF-8 text with metadata', () => {
|
|
97
|
+
test('encodes message + metadata', () => {
|
|
98
|
+
const encoded = encodeCrcV2TransferData(['Happy Birthday', 'https://app.gnosis.io'], 0x1001);
|
|
99
|
+
const hex = encoded.slice(2);
|
|
100
|
+
// Header starts with version=01, type=1001
|
|
101
|
+
expect(hex.slice(0, 2)).toBe('01');
|
|
102
|
+
expect(hex.slice(2, 6)).toBe('1001');
|
|
103
|
+
// Contains separator 480a868a
|
|
104
|
+
expect(hex).toContain('480a868a');
|
|
105
|
+
});
|
|
106
|
+
test('round-trips message + metadata', () => {
|
|
107
|
+
const message = 'Happy Birthday';
|
|
108
|
+
const metadata = 'https://app.gnosis.io';
|
|
109
|
+
const encoded = encodeCrcV2TransferData([message, metadata], 0x1001);
|
|
110
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
111
|
+
expect(decoded.type).toBe(0x1001);
|
|
112
|
+
const p = decoded.payload;
|
|
113
|
+
expect(p.message).toBe(message);
|
|
114
|
+
expect(p.metadata).toBe(metadata);
|
|
115
|
+
});
|
|
116
|
+
test('length equals message + separator + metadata byte count', () => {
|
|
117
|
+
const message = 'Hello';
|
|
118
|
+
const metadata = 'order:123';
|
|
119
|
+
const encoded = encodeCrcV2TransferData([message, metadata], 0x1001);
|
|
120
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
121
|
+
const messageBytes = new TextEncoder().encode(message).length;
|
|
122
|
+
const metadataBytes = new TextEncoder().encode(metadata).length;
|
|
123
|
+
// 4 bytes for the separator
|
|
124
|
+
expect(decoded.length).toBe(messageBytes + 4 + metadataBytes);
|
|
125
|
+
});
|
|
126
|
+
test('round-trips with emoji in message and metadata', () => {
|
|
127
|
+
const message = 'Thanks! 🎉';
|
|
128
|
+
const metadata = 'app:circles:tag=gift 🎁';
|
|
129
|
+
const encoded = encodeCrcV2TransferData([message, metadata], 0x1001);
|
|
130
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
131
|
+
const p = decoded.payload;
|
|
132
|
+
expect(p.message).toBe(message);
|
|
133
|
+
expect(p.metadata).toBe(metadata);
|
|
134
|
+
});
|
|
135
|
+
test('throws on decode if separator is missing', () => {
|
|
136
|
+
// Manually craft a 0x1001 payload without the separator
|
|
137
|
+
const fakePayloadHex = '48656c6c6f'; // "Hello" without separator
|
|
138
|
+
const length = fakePayloadHex.length / 2;
|
|
139
|
+
const hex = '0x' + '01' + '1001' + length.toString(16).padStart(4, '0') + fakePayloadHex;
|
|
140
|
+
expect(() => decodeCrcV2TransferData(hex)).toThrow(/separator/i);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Type 0x0004 — ABI-encoded calldata
|
|
145
|
+
// ============================================================================
|
|
146
|
+
describe('Type 0x0004 — ABI-encoded calldata', () => {
|
|
147
|
+
const signature = 'transfer(address,uint256)';
|
|
148
|
+
const address = '0x742d35Cc6634C0532925a3b844Bc454e4438f44e';
|
|
149
|
+
const amount = 1000000000000000000n;
|
|
150
|
+
const expectedSelector = 'a9059cbb';
|
|
151
|
+
const expectedEncodedAddress = '000000000000000000000000742d35cc6634c0532925a3b844bc454e4438f44e';
|
|
152
|
+
const expectedEncodedAmount = '0000000000000000000000000000000000000000000000000de0b6b3a7640000';
|
|
153
|
+
test('encodes from function signature + params', () => {
|
|
154
|
+
const result = encodeCrcV2TransferData([signature, address, amount], 0x0004);
|
|
155
|
+
const hex = result.slice(2);
|
|
156
|
+
// Header: 01 0004 0044
|
|
157
|
+
expect(hex.slice(0, 10)).toBe('0100040044');
|
|
158
|
+
// Selector
|
|
159
|
+
expect(hex.slice(10, 18)).toBe(expectedSelector);
|
|
160
|
+
// Encoded address
|
|
161
|
+
expect(hex.slice(18, 18 + 64)).toBe(expectedEncodedAddress);
|
|
162
|
+
// Encoded amount
|
|
163
|
+
expect(hex.slice(18 + 64, 18 + 128)).toBe(expectedEncodedAmount);
|
|
164
|
+
});
|
|
165
|
+
test('encodes from pre-encoded calldata (single element)', () => {
|
|
166
|
+
const rawCalldata = `0x${expectedSelector}${expectedEncodedAddress}${expectedEncodedAmount}`;
|
|
167
|
+
const result = encodeCrcV2TransferData([rawCalldata], 0x0004);
|
|
168
|
+
const hex = result.slice(2);
|
|
169
|
+
expect(hex.slice(0, 10)).toBe('0100040044');
|
|
170
|
+
expect(hex.slice(10, 18)).toBe(expectedSelector);
|
|
171
|
+
});
|
|
172
|
+
test('decodes known selector with resolved signature and params', () => {
|
|
173
|
+
const encoded = encodeCrcV2TransferData([signature, address, amount], 0x0004);
|
|
174
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
175
|
+
expect(decoded.type).toBe(0x0004);
|
|
176
|
+
expect(decoded.length).toBe(68); // 4 selector + 32 + 32
|
|
177
|
+
const p = decoded.payload;
|
|
178
|
+
expect(p.selector).toBe(`0x${expectedSelector}`);
|
|
179
|
+
expect(p.signature).toBe(signature);
|
|
180
|
+
expect(p.params.length).toBe(2);
|
|
181
|
+
// address param (lowercased with checksum)
|
|
182
|
+
expect(p.params[0].toLowerCase()).toBe(address.toLowerCase());
|
|
183
|
+
// uint256 param
|
|
184
|
+
expect(p.params[1]).toBe(amount);
|
|
185
|
+
});
|
|
186
|
+
test('decodes raw calldata with known selector', () => {
|
|
187
|
+
const rawCalldata = `0x${expectedSelector}${expectedEncodedAddress}${expectedEncodedAmount}`;
|
|
188
|
+
const encoded = encodeCrcV2TransferData([rawCalldata], 0x0004);
|
|
189
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
190
|
+
const p = decoded.payload;
|
|
191
|
+
expect(p.selector).toBe(`0x${expectedSelector}`);
|
|
192
|
+
expect(p.signature).toBe(signature);
|
|
193
|
+
expect(p.params.length).toBe(2);
|
|
194
|
+
});
|
|
195
|
+
test('returns raw data for unknown selector', () => {
|
|
196
|
+
// Craft calldata with a selector not in the lookup table
|
|
197
|
+
// Use 8 zeros padded — extremely unlikely to be a real function
|
|
198
|
+
const unknownSelector = 'ffffffff';
|
|
199
|
+
const fakePayload = unknownSelector + expectedEncodedAddress;
|
|
200
|
+
const encoded = encodeCrcV2TransferData([`0x${fakePayload}`], 0x0004);
|
|
201
|
+
const decoded = decodeCrcV2TransferData(encoded);
|
|
202
|
+
// Unknown selector — payload is raw hex string
|
|
203
|
+
expect(typeof decoded.payload).toBe('string');
|
|
204
|
+
expect(decoded.payload).toBe(`0x${fakePayload}`);
|
|
205
|
+
});
|
|
206
|
+
test('throws for invalid signature', () => {
|
|
207
|
+
expect(() => encodeCrcV2TransferData(['notASignature', address], 0x0004)).toThrow();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
// ============================================================================
|
|
211
|
+
// Version handling
|
|
212
|
+
// ============================================================================
|
|
213
|
+
describe('Version handling', () => {
|
|
214
|
+
test('default version is 0x01', () => {
|
|
215
|
+
const encoded = encodeCrcV2TransferData(['test'], 0x0001);
|
|
216
|
+
expect(encoded.slice(2, 4)).toBe('01');
|
|
217
|
+
});
|
|
218
|
+
test('throws on version mismatch when decoding', () => {
|
|
219
|
+
const encoded = encodeCrcV2TransferData(['test'], 0x0001, 0x01);
|
|
220
|
+
expect(() => decodeCrcV2TransferData(encoded, 0x02)).toThrow(/version/i);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// Error cases
|
|
225
|
+
// ============================================================================
|
|
226
|
+
describe('Error cases', () => {
|
|
227
|
+
test('throws for unsupported type in encode', () => {
|
|
228
|
+
expect(() => encodeCrcV2TransferData(['foo'], 0x9999)).toThrow();
|
|
229
|
+
});
|
|
230
|
+
test('throws for unsupported type in decode', () => {
|
|
231
|
+
// Manually craft an encoded payload with type 0x9999
|
|
232
|
+
const hex = '0x01999900020000';
|
|
233
|
+
expect(() => decodeCrcV2TransferData(hex)).toThrow();
|
|
234
|
+
});
|
|
235
|
+
test('throws for data too short to decode', () => {
|
|
236
|
+
expect(() => decodeCrcV2TransferData('0x0100')).toThrow(/too short/i);
|
|
237
|
+
});
|
|
238
|
+
test('throws for payload length mismatch', () => {
|
|
239
|
+
// Header says length=10 but no payload follows
|
|
240
|
+
const hex = '0x' + '01' + '0001' + '000a'; // 5 header bytes, claims 10 payload bytes
|
|
241
|
+
expect(() => decodeCrcV2TransferData(hex)).toThrow(/mismatch/i);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Hex, DecodedTransferData, TransferDataType } from '@aboutcircles/sdk-types';
|
|
2
|
+
export type { TransferDataType, DecodedTransferData, DecodedAbiPayload, DecodedMetadataPayload } from '@aboutcircles/sdk-types';
|
|
3
|
+
/**
|
|
4
|
+
* Encodes payload data for Circles V2 transfer annotations.
|
|
5
|
+
*
|
|
6
|
+
* Format: [version: 1B][type: 2B][length: 2B][payload bytes]
|
|
7
|
+
*
|
|
8
|
+
* Types:
|
|
9
|
+
* - 0x0001: UTF-8 text string
|
|
10
|
+
* - 0x1001: UTF-8 text with metadata (payload[0] = message, payload[1] = metadata string)
|
|
11
|
+
* - 0x0002: Raw 32-byte hex (e.g. XMTP message ID)
|
|
12
|
+
* - 0x0003: IPFS CID string (CIDv0 or CIDv1, converted to hex)
|
|
13
|
+
* - 0x0004: ABI-encoded calldata
|
|
14
|
+
* - payload[0] can be: a function signature string + params, or raw encoded hex calldata
|
|
15
|
+
*
|
|
16
|
+
* @param payload - Array of values; interpretation depends on type
|
|
17
|
+
* @param type - Transfer data type (0x0001–0x0004)
|
|
18
|
+
* @param version - Protocol version (default 0x01)
|
|
19
|
+
* @returns 0x-prefixed hex string
|
|
20
|
+
*/
|
|
21
|
+
export declare function encodeCrcV2TransferData(payload: unknown[], type: TransferDataType, version?: number): Hex;
|
|
22
|
+
/**
|
|
23
|
+
* Decodes Circles V2 transfer data annotation from hex.
|
|
24
|
+
*
|
|
25
|
+
* @param encoded - 0x-prefixed hex string (as returned from circles_getTransferData)
|
|
26
|
+
* @param version - Expected protocol version (default 0x01); throws if mismatch
|
|
27
|
+
* @returns Decoded struct with type, length, and payload
|
|
28
|
+
*/
|
|
29
|
+
export declare function decodeCrcV2TransferData(encoded: string, version?: number): DecodedTransferData;
|
|
30
|
+
//# sourceMappingURL=transferData.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transferData.d.ts","sourceRoot":"","sources":["../src/transferData.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,GAAG,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAM1F,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AA8HhI;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAAE,EAClB,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,MAAa,GACrB,GAAG,CA4FL;AAMD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,MAAa,GACrB,mBAAmB,CA+FrB"}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { CID } from 'multiformats/cid';
|
|
2
|
+
import { base16 } from 'multiformats/bases/base16';
|
|
3
|
+
import { base58btc } from 'multiformats/bases/base58';
|
|
4
|
+
import { keccak_256 } from '@noble/hashes/sha3.js';
|
|
5
|
+
import { encodeFunctionData } from './abi.js';
|
|
6
|
+
import { decodeAbiParameters } from './abi.js';
|
|
7
|
+
import { hexToBytes, bytesToHex } from './bytes.js';
|
|
8
|
+
import { EncodingError } from './errors.js';
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// CONSTANTS
|
|
11
|
+
// ============================================================================
|
|
12
|
+
const VERSION_BYTES = 1;
|
|
13
|
+
const TYPE_BYTES = 2;
|
|
14
|
+
const LENGTH_BYTES = 2;
|
|
15
|
+
const HEADER_BYTES = VERSION_BYTES + TYPE_BYTES + LENGTH_BYTES; // 5
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// HELPERS
|
|
18
|
+
// ============================================================================
|
|
19
|
+
function numToHex(value, byteLength) {
|
|
20
|
+
return value.toString(16).padStart(byteLength * 2, '0');
|
|
21
|
+
}
|
|
22
|
+
function hexNum(hex, offset, length) {
|
|
23
|
+
return parseInt(hex.slice(offset, offset + length * 2), 16);
|
|
24
|
+
}
|
|
25
|
+
function cidToHex(cid) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = CID.parse(cid, base58btc);
|
|
28
|
+
const v1 = parsed.toV1();
|
|
29
|
+
// base16 (hex) encoding includes 'f' prefix - strip it
|
|
30
|
+
const encoded = v1.toString(base16);
|
|
31
|
+
return encoded.startsWith('f') ? encoded.slice(1) : encoded;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Try as CIDv1 directly
|
|
35
|
+
const parsed = CID.parse(cid);
|
|
36
|
+
const v1 = parsed.toV1();
|
|
37
|
+
const encoded = v1.toString(base16);
|
|
38
|
+
return encoded.startsWith('f') ? encoded.slice(1) : encoded;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const METADATA_SEPARATOR = '480a868a'; // keccak256("metadataStart") first 4 bytes
|
|
42
|
+
function hexToCid(hex) {
|
|
43
|
+
const bytes = hexToBytes(hex.startsWith('0x') ? hex : `0x${hex}`);
|
|
44
|
+
const cid = CID.decode(bytes);
|
|
45
|
+
// Return CIDv0 if compatible (dag-pb + sha2-256), otherwise CIDv1
|
|
46
|
+
const isDagPb = cid.code === 0x70;
|
|
47
|
+
const isSha256 = cid.multihash.code === 0x12;
|
|
48
|
+
if (isDagPb && isSha256) {
|
|
49
|
+
return cid.toV0().toString();
|
|
50
|
+
}
|
|
51
|
+
return cid.toString();
|
|
52
|
+
}
|
|
53
|
+
// @todo construct lookup table based on the contracts in the core pkg
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// LOCAL SELECTOR LOOKUP TABLE
|
|
56
|
+
// ============================================================================
|
|
57
|
+
/** Known function signatures for local selector resolution. */
|
|
58
|
+
const KNOWN_SIGNATURES = [
|
|
59
|
+
// ERC20
|
|
60
|
+
'transfer(address,uint256)',
|
|
61
|
+
'transferFrom(address,address,uint256)',
|
|
62
|
+
'approve(address,uint256)',
|
|
63
|
+
'increaseAllowance(address,uint256)',
|
|
64
|
+
'decreaseAllowance(address,uint256)',
|
|
65
|
+
'balanceOf(address)',
|
|
66
|
+
'allowance(address,address)',
|
|
67
|
+
'totalSupply()',
|
|
68
|
+
'decimals()',
|
|
69
|
+
'permit(address,address,uint256,uint256,uint8,bytes32,bytes32)',
|
|
70
|
+
// ERC1155
|
|
71
|
+
'safeTransferFrom(address,address,uint256,uint256,bytes)',
|
|
72
|
+
'safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)',
|
|
73
|
+
'setApprovalForAll(address,bool)',
|
|
74
|
+
'isApprovedForAll(address,address)',
|
|
75
|
+
// Circles Hub V2
|
|
76
|
+
'trust(address,uint96)',
|
|
77
|
+
'isTrusted(address,address)',
|
|
78
|
+
'isHuman(address)',
|
|
79
|
+
'wrap(address,uint256,uint8)',
|
|
80
|
+
'toTokenId(address)',
|
|
81
|
+
'operateFlowMatrix(address[],tuple[],tuple[],bytes)',
|
|
82
|
+
// Wrapped Circles
|
|
83
|
+
'unwrap(uint256)',
|
|
84
|
+
'convertDemurrageToInflationaryValue(uint256,uint64)',
|
|
85
|
+
'convertInflationaryToDemurrageValue(uint256,uint64)',
|
|
86
|
+
// Safe
|
|
87
|
+
'enableModule(address)',
|
|
88
|
+
'isModuleEnabled(address)',
|
|
89
|
+
// Invitation
|
|
90
|
+
'trustInviter(address)',
|
|
91
|
+
'claimInvite()',
|
|
92
|
+
'claimInvites(uint256)',
|
|
93
|
+
'createAccount(address)',
|
|
94
|
+
'createAccounts(address[])',
|
|
95
|
+
// LiftERC20
|
|
96
|
+
'erc20Circles(uint8,address)',
|
|
97
|
+
];
|
|
98
|
+
/** Compute 4-byte selector from a function signature string. */
|
|
99
|
+
function computeSelector(sig) {
|
|
100
|
+
const hash = keccak_256(new TextEncoder().encode(sig));
|
|
101
|
+
return bytesToHex(hash.slice(0, 4)).slice(2); // strip 0x, 8 hex chars
|
|
102
|
+
}
|
|
103
|
+
/** Map from 8-char hex selector → full signature string. Built once at module load. */
|
|
104
|
+
const SELECTOR_TABLE = new Map(KNOWN_SIGNATURES.map(sig => [computeSelector(sig), sig]));
|
|
105
|
+
/**
|
|
106
|
+
* Parse a function signature string into param type strings.
|
|
107
|
+
* e.g. "transfer(address,uint256)" → ["address", "uint256"]
|
|
108
|
+
*/
|
|
109
|
+
function parseSignatureTypes(signature) {
|
|
110
|
+
const parenIdx = signature.indexOf('(');
|
|
111
|
+
if (parenIdx === -1)
|
|
112
|
+
return [];
|
|
113
|
+
const inner = signature.slice(parenIdx + 1, -1);
|
|
114
|
+
if (!inner)
|
|
115
|
+
return [];
|
|
116
|
+
return inner.split(',').map(t => t.trim()).filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// ENCODE
|
|
120
|
+
// ============================================================================
|
|
121
|
+
/**
|
|
122
|
+
* Encodes payload data for Circles V2 transfer annotations.
|
|
123
|
+
*
|
|
124
|
+
* Format: [version: 1B][type: 2B][length: 2B][payload bytes]
|
|
125
|
+
*
|
|
126
|
+
* Types:
|
|
127
|
+
* - 0x0001: UTF-8 text string
|
|
128
|
+
* - 0x1001: UTF-8 text with metadata (payload[0] = message, payload[1] = metadata string)
|
|
129
|
+
* - 0x0002: Raw 32-byte hex (e.g. XMTP message ID)
|
|
130
|
+
* - 0x0003: IPFS CID string (CIDv0 or CIDv1, converted to hex)
|
|
131
|
+
* - 0x0004: ABI-encoded calldata
|
|
132
|
+
* - payload[0] can be: a function signature string + params, or raw encoded hex calldata
|
|
133
|
+
*
|
|
134
|
+
* @param payload - Array of values; interpretation depends on type
|
|
135
|
+
* @param type - Transfer data type (0x0001–0x0004)
|
|
136
|
+
* @param version - Protocol version (default 0x01)
|
|
137
|
+
* @returns 0x-prefixed hex string
|
|
138
|
+
*/
|
|
139
|
+
export function encodeCrcV2TransferData(payload, type, version = 0x01) {
|
|
140
|
+
let payloadHex;
|
|
141
|
+
switch (type) {
|
|
142
|
+
case 0x0001: {
|
|
143
|
+
// UTF-8 text
|
|
144
|
+
const text = payload[0];
|
|
145
|
+
const bytes = new TextEncoder().encode(text);
|
|
146
|
+
payloadHex = bytesToHex(bytes).slice(2);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 0x1001: {
|
|
150
|
+
// UTF-8 text with metadata
|
|
151
|
+
const message = payload[0];
|
|
152
|
+
const metadata = payload[1];
|
|
153
|
+
const messageHex = bytesToHex(new TextEncoder().encode(message)).slice(2);
|
|
154
|
+
const metadataHex = bytesToHex(new TextEncoder().encode(metadata)).slice(2);
|
|
155
|
+
payloadHex = messageHex + METADATA_SEPARATOR + metadataHex;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 0x0002: {
|
|
159
|
+
// Raw hex (XMTP message ID - 32 bytes)
|
|
160
|
+
const raw = payload[0];
|
|
161
|
+
const cleaned = raw.startsWith('0x') ? raw.slice(2) : raw;
|
|
162
|
+
if (cleaned.length !== 64) {
|
|
163
|
+
throw new EncodingError(`Type 0x0002 expects a 32-byte hex value (64 hex chars), got ${cleaned.length / 2} bytes`, {
|
|
164
|
+
code: 'ENCODING_INVALID_LENGTH',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
payloadHex = cleaned.toLowerCase();
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 0x0003: {
|
|
171
|
+
// IPFS CID
|
|
172
|
+
const cidStr = payload[0];
|
|
173
|
+
payloadHex = cidToHex(cidStr);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 0x0004: {
|
|
177
|
+
// ABI-encoded calldata
|
|
178
|
+
const first = payload[0];
|
|
179
|
+
if (payload.length === 1 && first.startsWith('0x')) {
|
|
180
|
+
// Raw pre-encoded calldata
|
|
181
|
+
payloadHex = first.slice(2).toLowerCase();
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Function signature + params
|
|
185
|
+
const signature = first; // e.g. "transfer(address,uint256)"
|
|
186
|
+
const parenIdx = signature.indexOf('(');
|
|
187
|
+
if (parenIdx === -1) {
|
|
188
|
+
throw new EncodingError('Type 0x0004 payload[0] must be a function signature like "transfer(address,uint256)"', {
|
|
189
|
+
code: 'ENCODING_INVALID_SIGNATURE',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const fnName = signature.slice(0, parenIdx);
|
|
193
|
+
const paramTypes = signature
|
|
194
|
+
.slice(parenIdx + 1, -1)
|
|
195
|
+
.split(',')
|
|
196
|
+
.map(t => t.trim())
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
const abiInputs = paramTypes.map((t, i) => ({ type: t, name: `p${i}` }));
|
|
199
|
+
const args = payload.slice(1);
|
|
200
|
+
const encoded = encodeFunctionData({
|
|
201
|
+
abi: [{ name: fnName, type: 'function', stateMutability: 'nonpayable', inputs: abiInputs, outputs: [] }],
|
|
202
|
+
functionName: fnName,
|
|
203
|
+
args,
|
|
204
|
+
});
|
|
205
|
+
payloadHex = encoded.slice(2).toLowerCase();
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
default:
|
|
210
|
+
throw new EncodingError(`Unsupported transfer data type: ${type}`, {
|
|
211
|
+
code: 'ENCODING_UNSUPPORTED_TYPE',
|
|
212
|
+
context: { type },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const payloadBytes = payloadHex.length / 2;
|
|
216
|
+
const header = numToHex(version, VERSION_BYTES) +
|
|
217
|
+
numToHex(type, TYPE_BYTES) +
|
|
218
|
+
numToHex(payloadBytes, LENGTH_BYTES);
|
|
219
|
+
return `0x${header}${payloadHex}`;
|
|
220
|
+
}
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// DECODE
|
|
223
|
+
// ============================================================================
|
|
224
|
+
/**
|
|
225
|
+
* Decodes Circles V2 transfer data annotation from hex.
|
|
226
|
+
*
|
|
227
|
+
* @param encoded - 0x-prefixed hex string (as returned from circles_getTransferData)
|
|
228
|
+
* @param version - Expected protocol version (default 0x01); throws if mismatch
|
|
229
|
+
* @returns Decoded struct with type, length, and payload
|
|
230
|
+
*/
|
|
231
|
+
export function decodeCrcV2TransferData(encoded, version = 0x01) {
|
|
232
|
+
const hex = encoded.startsWith('0x') ? encoded.slice(2) : encoded;
|
|
233
|
+
if (hex.length < HEADER_BYTES * 2) {
|
|
234
|
+
throw new EncodingError('Encoded data too short to contain header', {
|
|
235
|
+
code: 'ENCODING_TOO_SHORT',
|
|
236
|
+
context: { encoded },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const encodedVersion = hexNum(hex, 0, VERSION_BYTES);
|
|
240
|
+
if (encodedVersion !== version) {
|
|
241
|
+
throw new EncodingError(`Version mismatch: expected ${version}, got ${encodedVersion}`, {
|
|
242
|
+
code: 'ENCODING_VERSION_MISMATCH',
|
|
243
|
+
context: { expected: version, got: encodedVersion },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const type = hexNum(hex, VERSION_BYTES * 2, TYPE_BYTES);
|
|
247
|
+
const length = hexNum(hex, (VERSION_BYTES + TYPE_BYTES) * 2, LENGTH_BYTES);
|
|
248
|
+
const payloadHex = hex.slice(HEADER_BYTES * 2, HEADER_BYTES * 2 + length * 2);
|
|
249
|
+
if (payloadHex.length !== length * 2) {
|
|
250
|
+
throw new EncodingError('Payload length mismatch', {
|
|
251
|
+
code: 'ENCODING_LENGTH_MISMATCH',
|
|
252
|
+
context: { expected: length, got: payloadHex.length / 2 },
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
let payload;
|
|
256
|
+
switch (type) {
|
|
257
|
+
case 0x0001: {
|
|
258
|
+
// UTF-8 text
|
|
259
|
+
const bytes = hexToBytes(`0x${payloadHex}`);
|
|
260
|
+
payload = new TextDecoder().decode(bytes);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
case 0x1001: {
|
|
264
|
+
// UTF-8 text with metadata
|
|
265
|
+
const separatorIdx = payloadHex.indexOf(METADATA_SEPARATOR);
|
|
266
|
+
if (separatorIdx === -1) {
|
|
267
|
+
throw new EncodingError('Type 0x1001: metadata separator not found in payload', {
|
|
268
|
+
code: 'ENCODING_MISSING_SEPARATOR',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const messageHex = payloadHex.slice(0, separatorIdx);
|
|
272
|
+
const metadataHex = payloadHex.slice(separatorIdx + METADATA_SEPARATOR.length);
|
|
273
|
+
const message = new TextDecoder().decode(hexToBytes(`0x${messageHex}`));
|
|
274
|
+
const metadata = new TextDecoder().decode(hexToBytes(`0x${metadataHex}`));
|
|
275
|
+
payload = { message, metadata };
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 0x0002: {
|
|
279
|
+
// Raw hex
|
|
280
|
+
payload = `0x${payloadHex}`;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case 0x0003: {
|
|
284
|
+
// IPFS CID (hex → CID string)
|
|
285
|
+
payload = hexToCid(payloadHex);
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case 0x0004: {
|
|
289
|
+
// ABI-encoded calldata — try local selector lookup, fallback to raw data
|
|
290
|
+
const selectorHex = payloadHex.slice(0, 8);
|
|
291
|
+
const selector = `0x${selectorHex}`;
|
|
292
|
+
const signature = SELECTOR_TABLE.get(selectorHex);
|
|
293
|
+
if (signature) {
|
|
294
|
+
const paramTypes = parseSignatureTypes(signature);
|
|
295
|
+
const paramsHex = payloadHex.slice(8);
|
|
296
|
+
const params = paramTypes.length > 0
|
|
297
|
+
? decodeAbiParameters(paramTypes, paramsHex)
|
|
298
|
+
: [];
|
|
299
|
+
payload = { selector, signature, params };
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Unknown selector — return raw calldata hex
|
|
303
|
+
payload = `0x${payloadHex}`;
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
default:
|
|
308
|
+
throw new EncodingError(`Unsupported transfer data type: ${type}`, {
|
|
309
|
+
code: 'ENCODING_UNSUPPORTED_TYPE',
|
|
310
|
+
context: { type },
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return { type: type, length, payload };
|
|
314
|
+
}
|