@rhinestone/sdk 0.0.0-dev-20261008160531 → 0.0.0-dev-20261008160829

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.
@@ -3,7 +3,7 @@ import { createOrchestratorAuth } from './auth.js';
3
3
  import { ChainCatalog, parseChains } from './chain-catalog.js';
4
4
  import { fetchOrchestratorJson } from './fetch.js';
5
5
  import { mapIntentRequestToWire, mapIntentStatusFromWire, mapPortfolioFromWire, mapQuoteResponseFromWire, mapSignedIntentToWire, mapSplitRequestToWire, mapSplitResultFromWire, } from './mappers.js';
6
- const SDK_VERSION = '0.0.0-dev-20261008160531';
6
+ const SDK_VERSION = '0.0.0-dev-20261008160829';
7
7
  const API_VERSION = '2026-04.blanc';
8
8
  // Bounds how long a killed layer or pulled settlement address stays pinnable.
9
9
  const CHAIN_CATALOG_TTL_MS = 10 * 60 * 1000;
@@ -1,4 +1,4 @@
1
- import type { ScopedAction } from '../types.js';
1
+ import type { ScopedAction, UniversalActionPolicyParamRule } from '../types.js';
2
2
  import type { SettlementContext } from './types.js';
3
3
  /**
4
4
  * The most relay fee the key may send LayerZero's receiver, in USDC units: ~10x
@@ -6,6 +6,13 @@ import type { SettlementContext } from './types.js';
6
6
  */
7
7
  export declare const LZ_CCTP_MAX_RELAY_FEE = 1000000n;
8
8
  export declare const LZ_EXECUTE_SELECTOR: `0x${string}`;
9
+ type Rule = UniversalActionPolicyParamRule;
10
+ /**
11
+ * The same check on the same word. A usage-limited rule is never the same: one
12
+ * hoisted out of an OR would count on every call, not only when its branch runs.
13
+ */
14
+ export declare const sameRule: (a: Rule, b: Rule) => boolean;
9
15
  /** The execute call, pinned to the permit's destinations, recipients and cap. */
10
16
  export declare function scopeLz(ctx: SettlementContext): ScopedAction;
17
+ export {};
11
18
  //# sourceMappingURL=lz.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lz.d.ts","sourceRoot":"","sources":["../../../../../../modules/validators/smart-sessions/settlement/lz.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAEV,YAAY,EAEb,MAAM,UAAU,CAAA;AAGjB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAwBhD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,WAAa,CAAA;AAE/C,eAAO,MAAM,mBAAmB,eAE/B,CAAA;AA8ED,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,GAAG,EAAE,iBAAiB,GAAG,YAAY,CAkO5D"}
1
+ {"version":3,"file":"lz.d.ts","sourceRoot":"","sources":["../../../../../../modules/validators/smart-sessions/settlement/lz.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAEV,YAAY,EACZ,8BAA8B,EAC/B,MAAM,UAAU,CAAA;AAGjB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AA4BhD;;;GAGG;AACH,eAAO,MAAM,qBAAqB,WAAa,CAAA;AAE/C,eAAO,MAAM,mBAAmB,eAE/B,CAAA;AAWD,KAAK,IAAI,GAAG,8BAA8B,CAAA;AAmB1C;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,GAAG,IAAI,EAAE,GAAG,IAAI,YAKc,CAAA;AA2DvD,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,GAAG,EAAE,iBAAiB,GAAG,YAAY,CAyP5D"}
@@ -8,12 +8,16 @@ import { SettlementLayerRefusal, served } from './served.js';
8
8
  * quoteId)`, whose nested calls pull the tokens, bridge them and sweep the rest
9
9
  * back. LZMultiCall runs any call it is handed, so every nested call is pinned:
10
10
  * the array length, each element offset, target, data pointer and
11
- * length-plus-selector, and each argument the key could redirect. The API picks
12
- * one of three routes, so the policy accepts exactly these layouts:
11
+ * length-plus-selector, and each argument the key could redirect, except what
12
+ * LZMultiCall or the target already refuses (see `batch` and `pull`). The policy
13
+ * accepts two of the API's routes, in three layouts:
13
14
  *
14
- * - Stargate TAXI or BUS: delegateTransferFrom, approve(pool), pool.send, sweep.
15
15
  * - CCTP: delegateTransferFrom, transfer(fee), approve(TokenMessengerV2),
16
16
  * depositForBurn, sweep; to Plasma without the fee transfer.
17
+ * - Stargate TAXI: delegateTransferFrom, approve(pool), pool.send, sweep; only
18
+ * into a leg no CCTP route reaches. Where both exist CCTP is cheaper and
19
+ * faster, and the orchestrator must not plan Stargate. BUS, which the
20
+ * orchestrator does not plan, is refused.
17
21
  */
18
22
  /**
19
23
  * Circle's TokenMessengerV2, which the API's CCTP route approves and burns
@@ -33,6 +37,16 @@ const TRANSFER = toFunctionSelector('transfer(address,uint256)');
33
37
  const DEPOSIT_FOR_BURN = toFunctionSelector('depositForBurn(uint256,uint32,bytes32,address,bytes32,uint256,uint32)');
34
38
  const SWEEP = toFunctionSelector('sweep(address[],address)');
35
39
  const ceil32 = (n) => BigInt(Math.ceil(n / 32) * 32);
40
+ const and = (terms) => terms.reduceRight((right, left) => ({ type: 'and', left, right }));
41
+ /**
42
+ * The same check on the same word. A usage-limited rule is never the same: one
43
+ * hoisted out of an OR would count on every call, not only when its branch runs.
44
+ */
45
+ export const sameRule = (a, b) => a.usageLimit === undefined &&
46
+ b.usageLimit === undefined &&
47
+ a.condition === b.condition &&
48
+ a.calldataOffset === b.calldataOffset &&
49
+ BigInt(a.referenceValue) === BigInt(b.referenceValue);
36
50
  /**
37
51
  * The pins of an `execute` batch under the canonical encoding, below the
38
52
  * `calls` pointer every route shares, and each call's argument accessor.
@@ -45,10 +59,17 @@ function batch(calls) {
45
59
  const start = 0x60n + relative;
46
60
  const data = start + 128n;
47
61
  const at = (offset) => data + 4n + offset;
48
- rules.push(pinValue(0x60n + BigInt(i) * 32n, relative), pin(start, call.target), ...(call.payable ? [] : [pinValue(start + 32n, 0n)]), pinValue(start + 64n, 0x60n),
62
+ // No call's value is pinned: the send's is the messaging fee, and every
63
+ // other target reverts on value (a non-payable solc function, checked on
64
+ // every LZ source chain), which LZMultiCall bubbles.
65
+ rules.push(pinValue(0x60n + BigInt(i) * 32n, relative), pin(start, call.target), pinValue(start + 64n, 0x60n),
49
66
  // The word ending at the selector: the length's low 28 bytes, then the
50
67
  // selector. A length above 2^224 fails to decode, so this pins both.
51
- pinValue(data - 28n, (BigInt(call.length) << 32n) | BigInt(call.selector)), ...call.args(at));
68
+ ...(call.shapeChecked
69
+ ? []
70
+ : [
71
+ pinValue(data - 28n, (BigInt(call.length) << 32n) | BigInt(call.selector)),
72
+ ]), ...call.args(at));
52
73
  args.push(at);
53
74
  relative += 128n + ceil32(call.length);
54
75
  });
@@ -70,15 +91,15 @@ export function scopeLz(ctx) {
70
91
  const cap = (offset) => ctx.cap === undefined ? [] : [cumulativeCap(offset, ctx.cap)];
71
92
  const crossChain = (leg) => leg.chainId !== ctx.chainId;
72
93
  const recipient = (offset, leg) => leg.recipient === undefined ? [] : [pinWord(offset, pad(leg.recipient))];
94
+ // LZMultiCall routes a call to its own TransferDelegate through
95
+ // `_handleTransfer`, which reverts unless the data is 132 bytes of
96
+ // `delegateTransferFrom` pulling from the caller, the account.
73
97
  const pull = {
74
98
  target: transferDelegate,
75
99
  selector: DELEGATE_TRANSFER_FROM,
76
100
  length: 132,
77
- args: (at) => [
78
- pin(at(0n), token),
79
- pin(at(32n), account),
80
- pin(at(64n), multiCall),
81
- ],
101
+ shapeChecked: true,
102
+ args: (at) => [pin(at(0n), token), pin(at(64n), multiCall)],
82
103
  };
83
104
  const sweep = {
84
105
  target: multiCall,
@@ -99,13 +120,22 @@ export function scopeLz(ctx) {
99
120
  args: (at) => [pin(at(0n), spender)],
100
121
  });
101
122
  const routes = [];
123
+ const cctp = source.cctp;
124
+ const cctpFrom = cctp !== undefined && isAddressEqual(token, cctp.token);
125
+ // A route exists where both chains carry its block.
126
+ const cctpLinks = (leg) => cctpFrom && crossChain(leg) && servedLz(leg)?.cctp !== undefined;
127
+ // A leg whose token CCTP does not mint, such as a Stargate USDC, stays
128
+ // Stargate's.
129
+ const cctpReaches = (leg) => {
130
+ const dst = servedLz(leg)?.cctp;
131
+ return cctpLinks(leg) && !!dst && isAddressEqual(leg.token, dst.token);
132
+ };
102
133
  const stargate = source.stargateUsdc;
103
134
  if (stargate !== undefined && isAddressEqual(token, stargate.token)) {
104
135
  const send = {
105
136
  target: stargate.pool,
106
137
  selector: OFT_SEND_SELECTOR,
107
138
  length: 484,
108
- payable: true,
109
139
  args: (at) => [
110
140
  pinValue(at(SEND.sendParamPointer), 0x80n),
111
141
  // The fee is native only; the refund returns to LZMultiCall, whose
@@ -124,28 +154,15 @@ export function scopeLz(ctx) {
124
154
  const calls = [pull, approve(stargate.pool), send, sweep];
125
155
  const { rules, args } = batch(calls);
126
156
  const s = args[2];
127
- // TAXI: extraOptions 0x0003, composeMsg and oftCmd empty. BUS: extraOptions
128
- // and composeMsg empty, oftCmd 0x01. No native drop, no compose.
129
- const taxi = [
130
- pinValue(s(SEND.composeMsgPointer), 0x120n),
131
- pinValue(s(SEND.oftCmdPointer), 0x140n),
132
- pinValue(s(0x160n), 2n),
133
- pinValue(s(0x180n), 0x0003n << 240n),
134
- pinValue(s(0x1a0n), 0n),
135
- pinValue(s(0x1c0n), 0n),
136
- ];
137
- const bus = [
138
- pinValue(s(SEND.composeMsgPointer), 0x100n),
139
- pinValue(s(SEND.oftCmdPointer), 0x120n),
140
- pinValue(s(0x160n), 0n),
141
- pinValue(s(0x180n), 0n),
142
- pinValue(s(0x1a0n), 1n),
143
- pinValue(s(0x1c0n), 0x01n << 248n),
144
- ];
145
- const reaches = (leg) => crossChain(leg) && servedLz(leg)?.stargateUsdc !== undefined;
157
+ // TAXI: extraOptions 0x0003, composeMsg and oftCmd empty. No native drop,
158
+ // no compose.
159
+ rules.push(pinValue(s(SEND.composeMsgPointer), 0x120n), pinValue(s(SEND.oftCmdPointer), 0x140n), pinValue(s(0x160n), 2n), pinValue(s(0x180n), 0x0003n << 240n), pinValue(s(0x1a0n), 0n), pinValue(s(0x1c0n), 0n));
160
+ const reaches = (leg) => crossChain(leg) &&
161
+ servedLz(leg)?.stargateUsdc !== undefined &&
162
+ !cctpReaches(leg);
146
163
  routes.push({
147
164
  rules,
148
- modes: [taxi, bus],
165
+ calls: calls.length,
149
166
  reaches,
150
167
  leg: (leg) => {
151
168
  const dst = servedLz(leg)?.stargateUsdc;
@@ -159,14 +176,15 @@ export function scopeLz(ctx) {
159
176
  limits: cap(args[0](96n)),
160
177
  });
161
178
  }
162
- const cctp = source.cctp;
163
- if (cctp !== undefined && isAddressEqual(token, cctp.token)) {
179
+ if (cctpFrom) {
164
180
  const burn = {
165
181
  target: LZ_CCTP_TOKEN_MESSENGER,
166
182
  selector: DEPOSIT_FOR_BURN,
167
183
  length: 228,
184
+ // `burnToken` is not pinned: the pull and the sweep are pinned to this
185
+ // token and the account, so another burn token can only spend what a
186
+ // third party left in LZMultiCall, and the pull is swept back.
168
187
  args: (at) => [
169
- pin(at(96n), token),
170
188
  // A non-zero destinationCaller restricts who may mint; the API never
171
189
  // sets one.
172
190
  pinValue(at(128n), 0n),
@@ -187,11 +205,10 @@ export function scopeLz(ctx) {
187
205
  : [pull, fee, approve(LZ_CCTP_TOKEN_MESSENGER), burn, sweep];
188
206
  const { rules, args } = batch(calls);
189
207
  const b = args[calls.indexOf(burn)];
190
- const reaches = (leg) => crossChain(leg) &&
191
- servedLz(leg)?.cctp !== undefined &&
192
- (servedLz(leg)?.cctp?.feeless === true) === feeless;
208
+ const reaches = (leg) => cctpLinks(leg) && (servedLz(leg)?.cctp?.feeless === true) === feeless;
193
209
  routes.push({
194
210
  rules,
211
+ calls: calls.length,
195
212
  reaches,
196
213
  leg: (leg) => {
197
214
  const dst = servedLz(leg)?.cctp;
@@ -230,17 +247,30 @@ export function scopeLz(ctx) {
230
247
  if (pinned.length === 0)
231
248
  return [];
232
249
  return [
233
- [
234
- allOf(route.rules),
235
- ...(route.modes ? [anyOf(route.modes.map(allOf))] : []),
236
- anyOf(pinned.map(allOf)),
237
- ...(route.limits.length ? [allOf(route.limits)] : []),
238
- ].reduceRight((right, left) => ({ type: 'and', left, right })),
250
+ {
251
+ route,
252
+ rest: [
253
+ anyOf(pinned.map(allOf)),
254
+ ...(route.limits.length ? [allOf(route.limits)] : []),
255
+ ],
256
+ },
239
257
  ];
240
258
  });
241
259
  if (branches.length === 0) {
242
260
  throw new SettlementLayerRefusal(`crossChainPermits: LZ has no route from chain ${ctx.chainId} to any \`to\` chain`);
243
261
  }
262
+ // A pure pin that every branch of a group carries (`group.every`) is checked
263
+ // once, ahead of their OR; that alone makes it sound. Grouping by call count
264
+ // only picks the batches likely to share pins.
265
+ const groups = [...new Set(branches.map(({ route }) => route.calls))].map((calls) => {
266
+ const group = branches.filter(({ route }) => route.calls === calls);
267
+ const shared = group[0].route.rules.filter((rule) => group.every(({ route }) => route.rules.some((r) => sameRule(r, rule))));
268
+ const own = group.map(({ route, rest }) => {
269
+ const left = route.rules.filter((r) => !shared.some((s) => sameRule(s, r)));
270
+ return and([...(left.length ? [allOf(left)] : []), ...rest]);
271
+ });
272
+ return and([...(shared.length ? [allOf(shared)] : []), anyOf(own)]);
273
+ });
244
274
  return {
245
275
  target: multiCall,
246
276
  selector: LZ_EXECUTE_SELECTOR,
@@ -258,7 +288,7 @@ export function scopeLz(ctx) {
258
288
  // stale TransferDelegate allowance could fund a second route, and a
259
289
  // second Stargate send would pay another native fee.
260
290
  left: allOf([{ ...pinValue(0n, 0x40n), usageLimit: 0x40n }]),
261
- right: anyOf(branches),
291
+ right: anyOf(groups),
262
292
  },
263
293
  },
264
294
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinestone/sdk",
3
- "version": "0.0.0-dev-20261008160531",
3
+ "version": "0.0.0-dev-20261008160829",
4
4
  "description": "Smart wallets and multichain transactions for your application",
5
5
  "author": {
6
6
  "name": "Rhinestone",