From 2e14f9e9f01b183d3f3eed5a77bfc9bedc84b9d3 Mon Sep 17 00:00:00 2001 From: M-Picco Date: Wed, 8 Jul 2026 14:30:59 -0300 Subject: [PATCH 1/2] fix(cli): fix xrpl onboard memo token id encoding; add emitter-kind argument to relay subcommand for core or generated addresses --- cli/src/__tests__/xrpl-onboarding.test.ts | 27 ++++++++++------------- cli/src/commands/xrpl/README.md | 15 ++++++++----- cli/src/commands/xrpl/relay.ts | 20 +++++++++++++++-- cli/src/xrpl/onboarding.ts | 23 +++++++++++-------- 4 files changed, 53 insertions(+), 32 deletions(-) diff --git a/cli/src/__tests__/xrpl-onboarding.test.ts b/cli/src/__tests__/xrpl-onboarding.test.ts index 38a895440..032b44eb7 100644 --- a/cli/src/__tests__/xrpl-onboarding.test.ts +++ b/cli/src/__tests__/xrpl-onboarding.test.ts @@ -33,28 +33,25 @@ describe("buildInitData", () => { expect(buildInitData(8, { type: "xrp" })).toBe("08"); }); - test("iou: decimals + 0x01 + currency + issuer, right-padded to 43 bytes total", () => { + test("iou: decimals + 0x01 + currency + issuer is exactly 42 bytes", () => { const out = buildInitData(9, { type: "iou", currency: FOO_CURRENCY, issuer: ADMIN, }); const issuerHex = Buffer.from(decodeAccountID(ADMIN)).toString("hex"); - // 1 (decimals) + 42 (token_id padded) = 43 bytes => 86 hex chars - expect(out.length).toBe(86); - expect( - out.startsWith("09" + "01" + FOO_CURRENCY.toLowerCase() + issuerHex) - ).toBe(true); - // trailing byte is zero padding (41 bytes of data -> padded to 42) - expect(out.endsWith("00")).toBe(true); + // 1 (decimals) + 1 (type) + 20 (currency) + 20 (issuer) = 42 bytes exactly, + // no padding — the Sequencer rejects any other init_data length. + expect(out.length).toBe(84); + expect(out).toBe("09" + "01" + FOO_CURRENCY.toLowerCase() + issuerHex); }); - test("mpt: decimals + 0x02 + mpt_id, right-padded to 43 bytes total", () => { + test("mpt: decimals + 0x02 + mpt_id, right-padded to 42 bytes total", () => { const out = buildInitData(9, { type: "mpt", mptId: MPT_ID }); - expect(out.length).toBe(86); + expect(out.length).toBe(84); expect(out.startsWith("09" + "02" + MPT_ID)).toBe(true); - // 1 + 2 + 48 = 51 hex of data, padded to 84 -> ends in zeros - expect(out.endsWith("0000")).toBe(true); + // 1 + 1 + 24 = 26 bytes of data, padded to 42 -> 16 zero bytes + expect(out.slice(52)).toBe("00".repeat(16)); }); test("mpt: rejects a wrong-length issuance id", () => { @@ -95,7 +92,7 @@ describe("buildOnboardingPayload", () => { expect(out.length).toBe(146); }); - test("full form (iou) is 115 bytes", () => { + test("full form (iou) is 114 bytes", () => { const initData = buildInitData(8, { type: "iou", currency: FOO_CURRENCY, @@ -108,8 +105,8 @@ describe("buildOnboardingPayload", () => { ticketCount: 2n, initData, }); - // 72 (fixed) + 43 (init_data) = 115 bytes => 230 hex chars - expect(out.length).toBe(230); + // 72 (fixed) + 42 (init_data) = 114 bytes => 228 hex chars + expect(out.length).toBe(228); }); }); diff --git a/cli/src/commands/xrpl/README.md b/cli/src/commands/xrpl/README.md index 5e62326ff..4e0f496e4 100644 --- a/cli/src/commands/xrpl/README.md +++ b/cli/src/commands/xrpl/README.md @@ -187,11 +187,11 @@ The memo carries: prefix `"XRPL"`, the `--admin` account, the app type (always ticket range (`--initial-ticket` / `--ticket-count`), and the token `init_data` (decimals + token identifier) selected via `--token`: -| `--token` | extra args | `init_data` tail | -| --------- | --------------------------------------------------------- | --------------------------------------------------------------------------- | -| `xrp` | `--decimals` (6) | `` (short form) | -| `iou` | `--decimals`, `--currency` (3-char or 40-hex), `--issuer` | `` + `0x01` + currency[20] + issuer[20], right-padded to 42 bytes | -| `mpt` | `--decimals`, `--mpt-id` (48-hex) | `` + `0x02` + mpt_id[24], right-padded to 42 bytes | +| `--token` | extra args | `init_data` (42 bytes total for iou/mpt) | +| --------- | --------------------------------------------------------- | -------------------------------------------------------------------- | +| `xrp` | `--decimals` (6) | `` (short form) | +| `iou` | `--decimals`, `--currency` (3-char or 40-hex), `--issuer` | `` + `0x01` + currency[20] + issuer[20] (exactly 42 bytes) | +| `mpt` | `--decimals`, `--mpt-id` (48-hex) | `` + `0x02` + mpt_id[24], right-padded to 42 bytes total | Other options: @@ -289,7 +289,10 @@ the relaying account (`--seed`). - `--tx-hash` (required) — XRPL tx that emitted the VAA - `--dst-chain` (required) — destination chain (name or id) -- `--request-type` — `ern1` (NTT transfer) or `erv1` (onboarding / register-peer) +- `--request-type` — `ERN1` (NTT transfer) or `ERV1` (onboarding / register-peer) +- `--emitter-kind` [`ERV1`] — `core` (default; a message the account published, + e.g. init / register-peer) or `generated` (a watcher ack for a custody tx — + XACK/XTCF, e.g. relaying a manual `TicketCreate` to the Sequencer) - `--dst-addr` — destination address (hex32; recipient NTT manager for `ern1`) - `--src-manager` / `--manager` — source NTT manager emitter (hex32), or derive it from the manager r-address/hex diff --git a/cli/src/commands/xrpl/relay.ts b/cli/src/commands/xrpl/relay.ts index 90436b89c..b465978b6 100644 --- a/cli/src/commands/xrpl/relay.ts +++ b/cli/src/commands/xrpl/relay.ts @@ -39,6 +39,7 @@ import { tokenIdFromFlags, tokenIdFromXrplAmount, xrplAccountToEmitter, + xrplGeneratedEmitter, type TokenId, } from "../../xrpl/tokenId"; import { withCommon } from "./common"; @@ -133,6 +134,13 @@ export function createXrplRelayCommand( choices: ["ERN1", "ERV1"] as const, default: "ERN1", }) + .option("emitter-kind", { + describe: + "ERV1 emitter namespace: core (a message the account published — init/register-peer) or generated (a watcher ack for a custody tx — XACK/XTCF, e.g. a TicketCreate)", + type: "string", + choices: ["core", "generated"] as const, + default: "core", + }) .option("dst-addr", { describe: "Destination address — chain-native (e.g. Solana base58, EVM hex) or 32-byte universal hex. For ERN1 this is the recipient NTT manager.", @@ -270,14 +278,22 @@ export async function runRelay( let emitterHex: string; if (requestType === RequestPrefix.ERV1) { - // Core VAA: emitter = the publishing (SENDER) account, left-padded to 32B. + // The emitter is derived from the SENDER account, in one of two + // namespaces: `core` (00×12 + account) for a message the account + // published itself via a core memo (init / register-peer), or + // `generated` ("XRPL" + 00×8 + account) for an ack the watcher + // synthesizes for a custody tx (XACK / XTCF). const sender: string | undefined = tx.Account ?? tx.tx_json?.Account; if (!sender) { throw new Error( "Could not determine the publishing account (tx.Account)" ); } - emitterHex = xrplAccountToEmitter(Buffer.from(decodeAccountID(sender))); + const senderId = Buffer.from(decodeAccountID(sender)); + emitterHex = + argv["emitter-kind"] === "generated" + ? xrplGeneratedEmitter(senderId) + : xrplAccountToEmitter(senderId); } else { // NTT transfer: emitter = keccak256("ntt" || custody || token), where the // custody account is the tx destination and the token is the delivered asset. diff --git a/cli/src/xrpl/onboarding.ts b/cli/src/xrpl/onboarding.ts index f325d9dfb..a05caf8c6 100644 --- a/cli/src/xrpl/onboarding.ts +++ b/cli/src/xrpl/onboarding.ts @@ -37,8 +37,12 @@ export function getDefaultCoreAccountForNetwork(network: Network): string { const TOKEN_TYPE_IOU = "01"; const TOKEN_TYPE_MPT = "02"; -// Size (in bytes) the token_id portion of init_data is right-padded to. -const TOKEN_ID_PADDED_BYTES = 42; +// Total size (in bytes) of an IOU/MPT init_data: decimals[1] + token type[1] + +// token id, right-padded so the WHOLE init_data is 42 bytes. An IOU +// (1+1+20+20) fills it exactly; an MPT (1+1+24) gets 16 bytes of padding. +// The Sequencer's InitializeXrplAccount enforces this length strictly — a +// 43-byte init_data is rejected with InvalidNttPayload. +const INIT_DATA_PADDED_BYTES = 42; export type TokenInit = | { type: "xrp" } @@ -110,8 +114,8 @@ export function currencyToHex40(currency: string): string { /** * Build the NTT/WTT `init_data` tail (hex, no 0x prefix): * - xrp: short form — just the decimals byte. - * - iou: decimals byte + (0x01 + currency[20] + issuer[20]) right-padded to 42 bytes. - * - mpt: decimals byte + (0x02 + mpt_issuance_id[24]) right-padded to 42 bytes. + * - iou: decimals + 0x01 + currency[20] + issuer[20] — exactly 42 bytes. + * - mpt: decimals + 0x02 + mpt_issuance_id[24], right-padded to 42 bytes. */ export function buildInitData(decimals: number, token: TokenInit): string { const dec = u8Hex(decimals); @@ -121,17 +125,18 @@ export function buildInitData(decimals: number, token: TokenInit): string { case "iou": { const currency = currencyToHex40(token.currency); const issuer = accountIdHex(token.issuer); - return ( - dec + - padRight(TOKEN_TYPE_IOU + currency + issuer, TOKEN_ID_PADDED_BYTES) + return padRight( + dec + TOKEN_TYPE_IOU + currency + issuer, + INIT_DATA_PADDED_BYTES ); } case "mpt": { if (!/^[0-9a-fA-F]{48}$/.test(token.mptId)) { throw new Error("expected mpt issuance id length of 48 (hex)"); } - return ( - dec + padRight(TOKEN_TYPE_MPT + token.mptId, TOKEN_ID_PADDED_BYTES) + return padRight( + dec + TOKEN_TYPE_MPT + token.mptId, + INIT_DATA_PADDED_BYTES ); } } From 63f2b4c48dcad039d084fc7b92b1abdf232f520d Mon Sep 17 00:00:00 2001 From: M-Picco Date: Wed, 8 Jul 2026 15:01:32 -0300 Subject: [PATCH 2/2] fix comment tags for foundry-toolchain on workflows --- .github/workflows/cli.yml | 2 +- .github/workflows/evm.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index cd162b44f..5d04ecc6a 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - name: Install Foundry - uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1 + uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1.8.0 with: version: v1.5.0 diff --git a/.github/workflows/evm.yml b/.github/workflows/evm.yml index 95d284dba..2595680d6 100644 --- a/.github/workflows/evm.yml +++ b/.github/workflows/evm.yml @@ -61,7 +61,7 @@ jobs: persist-credentials: false - name: Install Foundry - uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1 + uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1.8.0 with: version: v1.5.0 @@ -80,7 +80,7 @@ jobs: persist-credentials: false - name: Install Foundry - uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1 + uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1.8.0 with: version: v1.5.0 @@ -114,7 +114,7 @@ jobs: persist-credentials: false - name: Install Foundry - uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1 + uses: foundry-rs/foundry-toolchain@c7450ba673e133f5ee30098b3b54f444d3a2ca2d # v1.8.0 with: version: v1.5.0