Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/evm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
27 changes: 12 additions & 15 deletions cli/src/__tests__/xrpl-onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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,
Expand All @@ -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);
});
});

Expand Down
15 changes: 9 additions & 6 deletions cli/src/commands/xrpl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | `<decimals>` (short form) |
| `iou` | `--decimals`, `--currency` (3-char or 40-hex), `--issuer` | `<decimals>` + `0x01` + currency[20] + issuer[20], right-padded to 42 bytes |
| `mpt` | `--decimals`, `--mpt-id` (48-hex) | `<decimals>` + `0x02` + mpt_id[24], right-padded to 42 bytes |
| `--token` | extra args | `init_data` (42 bytes total for iou/mpt) |
| --------- | --------------------------------------------------------- | -------------------------------------------------------------------- |
| `xrp` | `--decimals` (6) | `<decimals>` (short form) |
| `iou` | `--decimals`, `--currency` (3-char or 40-hex), `--issuer` | `<decimals>` + `0x01` + currency[20] + issuer[20] (exactly 42 bytes) |
| `mpt` | `--decimals`, `--mpt-id` (48-hex) | `<decimals>` + `0x02` + mpt_id[24], right-padded to 42 bytes total |

Other options:

Expand Down Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions cli/src/commands/xrpl/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
tokenIdFromFlags,
tokenIdFromXrplAmount,
xrplAccountToEmitter,
xrplGeneratedEmitter,
type TokenId,
} from "../../xrpl/tokenId";
import { withCommon } from "./common";
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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.
Expand Down
23 changes: 14 additions & 9 deletions cli/src/xrpl/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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);
Expand All @@ -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
);
}
}
Expand Down
Loading