@tabai/sdk 0.2.0

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.
Files changed (204) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +401 -0
  3. package/bin/tab.mjs +23 -0
  4. package/dist/_shared/abi.d.ts +150 -0
  5. package/dist/_shared/abi.d.ts.map +1 -0
  6. package/dist/_shared/abi.js +197 -0
  7. package/dist/_shared/abi.js.map +1 -0
  8. package/dist/_shared/chains.d.ts +118 -0
  9. package/dist/_shared/chains.d.ts.map +1 -0
  10. package/dist/_shared/chains.js +89 -0
  11. package/dist/_shared/chains.js.map +1 -0
  12. package/dist/_shared/hex.d.ts +35 -0
  13. package/dist/_shared/hex.d.ts.map +1 -0
  14. package/dist/_shared/hex.js +40 -0
  15. package/dist/_shared/hex.js.map +1 -0
  16. package/dist/_shared/index.d.ts +14 -0
  17. package/dist/_shared/index.d.ts.map +1 -0
  18. package/dist/_shared/index.js +14 -0
  19. package/dist/_shared/index.js.map +1 -0
  20. package/dist/_shared/keccak256.d.ts +29 -0
  21. package/dist/_shared/keccak256.d.ts.map +1 -0
  22. package/dist/_shared/keccak256.js +145 -0
  23. package/dist/_shared/keccak256.js.map +1 -0
  24. package/dist/_shared/result.d.ts +78 -0
  25. package/dist/_shared/result.d.ts.map +1 -0
  26. package/dist/_shared/result.js +61 -0
  27. package/dist/_shared/result.js.map +1 -0
  28. package/dist/cli/client-config.d.ts +155 -0
  29. package/dist/cli/client-config.d.ts.map +1 -0
  30. package/dist/cli/client-config.js +382 -0
  31. package/dist/cli/client-config.js.map +1 -0
  32. package/dist/cli/connect.d.ts +76 -0
  33. package/dist/cli/connect.d.ts.map +1 -0
  34. package/dist/cli/connect.js +158 -0
  35. package/dist/cli/connect.js.map +1 -0
  36. package/dist/cli/doctor.d.ts +57 -0
  37. package/dist/cli/doctor.d.ts.map +1 -0
  38. package/dist/cli/doctor.js +253 -0
  39. package/dist/cli/doctor.js.map +1 -0
  40. package/dist/cli/index.d.ts +13 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +13 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/cli/main.d.ts +45 -0
  45. package/dist/cli/main.d.ts.map +1 -0
  46. package/dist/cli/main.js +371 -0
  47. package/dist/cli/main.js.map +1 -0
  48. package/dist/errors.d.ts +29 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +37 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/http/client-402.d.ts +243 -0
  53. package/dist/http/client-402.d.ts.map +1 -0
  54. package/dist/http/client-402.js +515 -0
  55. package/dist/http/client-402.js.map +1 -0
  56. package/dist/http/headers.d.ts +173 -0
  57. package/dist/http/headers.d.ts.map +1 -0
  58. package/dist/http/headers.js +284 -0
  59. package/dist/http/headers.js.map +1 -0
  60. package/dist/http/index.d.ts +15 -0
  61. package/dist/http/index.d.ts.map +1 -0
  62. package/dist/http/index.js +15 -0
  63. package/dist/http/index.js.map +1 -0
  64. package/dist/http/metering-claim.d.ts +82 -0
  65. package/dist/http/metering-claim.d.ts.map +1 -0
  66. package/dist/http/metering-claim.js +99 -0
  67. package/dist/http/metering-claim.js.map +1 -0
  68. package/dist/index.d.ts +48 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +51 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/logger.d.ts +40 -0
  73. package/dist/logger.d.ts.map +1 -0
  74. package/dist/logger.js +50 -0
  75. package/dist/logger.js.map +1 -0
  76. package/dist/mcp/assets.d.ts +31 -0
  77. package/dist/mcp/assets.d.ts.map +1 -0
  78. package/dist/mcp/assets.js +78 -0
  79. package/dist/mcp/assets.js.map +1 -0
  80. package/dist/mcp/index.d.ts +19 -0
  81. package/dist/mcp/index.d.ts.map +1 -0
  82. package/dist/mcp/index.js +19 -0
  83. package/dist/mcp/index.js.map +1 -0
  84. package/dist/mcp/json-schema.d.ts +86 -0
  85. package/dist/mcp/json-schema.d.ts.map +1 -0
  86. package/dist/mcp/json-schema.js +215 -0
  87. package/dist/mcp/json-schema.js.map +1 -0
  88. package/dist/mcp/json.d.ts +43 -0
  89. package/dist/mcp/json.d.ts.map +1 -0
  90. package/dist/mcp/json.js +69 -0
  91. package/dist/mcp/json.js.map +1 -0
  92. package/dist/mcp/registry-client.d.ts +88 -0
  93. package/dist/mcp/registry-client.d.ts.map +1 -0
  94. package/dist/mcp/registry-client.js +158 -0
  95. package/dist/mcp/registry-client.js.map +1 -0
  96. package/dist/mcp/schemas.d.ts +82 -0
  97. package/dist/mcp/schemas.d.ts.map +1 -0
  98. package/dist/mcp/schemas.js +493 -0
  99. package/dist/mcp/schemas.js.map +1 -0
  100. package/dist/mcp/server.d.ts +97 -0
  101. package/dist/mcp/server.d.ts.map +1 -0
  102. package/dist/mcp/server.js +285 -0
  103. package/dist/mcp/server.js.map +1 -0
  104. package/dist/mcp/settings.d.ts +90 -0
  105. package/dist/mcp/settings.d.ts.map +1 -0
  106. package/dist/mcp/settings.js +160 -0
  107. package/dist/mcp/settings.js.map +1 -0
  108. package/dist/mcp/toolset.d.ts +231 -0
  109. package/dist/mcp/toolset.d.ts.map +1 -0
  110. package/dist/mcp/toolset.js +760 -0
  111. package/dist/mcp/toolset.js.map +1 -0
  112. package/dist/payments/abi.d.ts +9 -0
  113. package/dist/payments/abi.d.ts.map +1 -0
  114. package/dist/payments/abi.js +17 -0
  115. package/dist/payments/abi.js.map +1 -0
  116. package/dist/payments/config.d.ts +199 -0
  117. package/dist/payments/config.d.ts.map +1 -0
  118. package/dist/payments/config.js +259 -0
  119. package/dist/payments/config.js.map +1 -0
  120. package/dist/payments/index.d.ts +13 -0
  121. package/dist/payments/index.d.ts.map +1 -0
  122. package/dist/payments/index.js +13 -0
  123. package/dist/payments/index.js.map +1 -0
  124. package/dist/payments/kuru.d.ts +191 -0
  125. package/dist/payments/kuru.d.ts.map +1 -0
  126. package/dist/payments/kuru.js +377 -0
  127. package/dist/payments/kuru.js.map +1 -0
  128. package/dist/payments/monad.d.ts +69 -0
  129. package/dist/payments/monad.d.ts.map +1 -0
  130. package/dist/payments/monad.js +306 -0
  131. package/dist/payments/monad.js.map +1 -0
  132. package/dist/payments/permit2.d.ts +118 -0
  133. package/dist/payments/permit2.d.ts.map +1 -0
  134. package/dist/payments/permit2.js +366 -0
  135. package/dist/payments/permit2.js.map +1 -0
  136. package/dist/payments/registry.d.ts +119 -0
  137. package/dist/payments/registry.d.ts.map +1 -0
  138. package/dist/payments/registry.js +199 -0
  139. package/dist/payments/registry.js.map +1 -0
  140. package/dist/payments/strategy.d.ts +80 -0
  141. package/dist/payments/strategy.d.ts.map +1 -0
  142. package/dist/payments/strategy.js +103 -0
  143. package/dist/payments/strategy.js.map +1 -0
  144. package/dist/proxy/hooks.d.ts +90 -0
  145. package/dist/proxy/hooks.d.ts.map +1 -0
  146. package/dist/proxy/hooks.js +35 -0
  147. package/dist/proxy/hooks.js.map +1 -0
  148. package/dist/proxy/index.d.ts +9 -0
  149. package/dist/proxy/index.d.ts.map +1 -0
  150. package/dist/proxy/index.js +9 -0
  151. package/dist/proxy/index.js.map +1 -0
  152. package/dist/proxy/proxy.d.ts +156 -0
  153. package/dist/proxy/proxy.d.ts.map +1 -0
  154. package/dist/proxy/proxy.js +366 -0
  155. package/dist/proxy/proxy.js.map +1 -0
  156. package/dist/server/adapters/express.d.ts +89 -0
  157. package/dist/server/adapters/express.d.ts.map +1 -0
  158. package/dist/server/adapters/express.js +215 -0
  159. package/dist/server/adapters/express.js.map +1 -0
  160. package/dist/server/adapters/hono.d.ts +52 -0
  161. package/dist/server/adapters/hono.d.ts.map +1 -0
  162. package/dist/server/adapters/hono.js +61 -0
  163. package/dist/server/adapters/hono.js.map +1 -0
  164. package/dist/server/adapters/next.d.ts +52 -0
  165. package/dist/server/adapters/next.d.ts.map +1 -0
  166. package/dist/server/adapters/next.js +56 -0
  167. package/dist/server/adapters/next.js.map +1 -0
  168. package/dist/server/index.d.ts +30 -0
  169. package/dist/server/index.d.ts.map +1 -0
  170. package/dist/server/index.js +30 -0
  171. package/dist/server/index.js.map +1 -0
  172. package/dist/server/metering.d.ts +209 -0
  173. package/dist/server/metering.d.ts.map +1 -0
  174. package/dist/server/metering.js +365 -0
  175. package/dist/server/metering.js.map +1 -0
  176. package/dist/server/post-paid.d.ts +355 -0
  177. package/dist/server/post-paid.d.ts.map +1 -0
  178. package/dist/server/post-paid.js +512 -0
  179. package/dist/server/post-paid.js.map +1 -0
  180. package/dist/x402/client.d.ts +203 -0
  181. package/dist/x402/client.d.ts.map +1 -0
  182. package/dist/x402/client.js +337 -0
  183. package/dist/x402/client.js.map +1 -0
  184. package/dist/x402/hub.d.ts +79 -0
  185. package/dist/x402/hub.d.ts.map +1 -0
  186. package/dist/x402/hub.js +164 -0
  187. package/dist/x402/hub.js.map +1 -0
  188. package/dist/x402/index.d.ts +27 -0
  189. package/dist/x402/index.d.ts.map +1 -0
  190. package/dist/x402/index.js +27 -0
  191. package/dist/x402/index.js.map +1 -0
  192. package/dist/x402/proxy.d.ts +162 -0
  193. package/dist/x402/proxy.d.ts.map +1 -0
  194. package/dist/x402/proxy.js +198 -0
  195. package/dist/x402/proxy.js.map +1 -0
  196. package/dist/x402/server.d.ts +162 -0
  197. package/dist/x402/server.d.ts.map +1 -0
  198. package/dist/x402/server.js +306 -0
  199. package/dist/x402/server.js.map +1 -0
  200. package/dist/x402/wire.d.ts +104 -0
  201. package/dist/x402/wire.d.ts.map +1 -0
  202. package/dist/x402/wire.js +265 -0
  203. package/dist/x402/wire.js.map +1 -0
  204. package/package.json +61 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Gasless Settlement: the Agent signs, somebody else sends.
3
+ *
4
+ * `TabSettlement.settleWithPermit2` takes a Permit2 `PermitWitnessTransferFrom`
5
+ * signature from the Agent and lets any account submit it. The witness binds
6
+ * the Service, the Asset, the amount, the settlement surface and the chain, so
7
+ * a relayer cannot redirect or resize what the Agent authorised; the nonce
8
+ * stops a replay; the deadline bounds how long the signature is good for. What
9
+ * the relayer contributes is gas, and the natural relayer is the Service's own
10
+ * gateway, which wants to be paid.
11
+ *
12
+ * Two pieces live here. {@link signSettlementPermit} produces the signature
13
+ * and the exact arguments the contract takes, for any code that holds an
14
+ * Agent's signer. {@link createRelayedMonadStrategy} is a `PaymentStrategy`
15
+ * that signs and posts to a relay endpoint, so `tab_settle` needs no MON in
16
+ * the Agent's account at all once the one-time Permit2 approval is in place.
17
+ *
18
+ * The one prerequisite is that approval: Permit2 pulls the Asset with
19
+ * `transferFrom`, so the Agent must have approved Permit2 once on the Asset.
20
+ * That is one gas-paid transaction per Asset for the life of the account, and
21
+ * the strategy says so by name when it is missing rather than posting a
22
+ * signature the relayer would only watch revert.
23
+ */
24
+ import { Interface, MaxUint256, getAddress, hexlify, randomBytes } from "ethers";
25
+ import { PERMIT2_ADDRESS, PERMIT2_WITNESS_TRANSFER_FROM_PRIMARY_TYPE, PERMIT2_WITNESS_TRANSFER_FROM_TYPES, TAB_SETTLEMENT_PERMIT2_ABI, causeOf, isAddress, ok, permit2Domain, wrap, } from "../_shared/index.js";
26
+ import { defaultLogger } from "../logger.js";
27
+ import { chainError, upstreamError, validationError } from "../errors.js";
28
+ import { ERC20_ABI } from "./abi.js";
29
+ import { indexAssets, } from "./monad.js";
30
+ import { assetKey, validateAssetRef, validateSettleRequest, } from "./strategy.js";
31
+ export const RELAYED_MONAD_STRATEGY_ID = "monad-relayed";
32
+ /** How long a signature stays valid when the caller names no deadline: ten minutes. */
33
+ export const DEFAULT_PERMIT_TTL_SECONDS = 600;
34
+ const permitInterface = new Interface(TAB_SETTLEMENT_PERMIT2_ABI);
35
+ const erc20 = new Interface(ERC20_ABI);
36
+ /** A random 256-bit unordered nonce. Permit2 keys them per owner in a bitmap. */
37
+ export const randomPermitNonce = () => BigInt(hexlify(randomBytes(32)));
38
+ /**
39
+ * Signs one Settlement under Permit2's domain.
40
+ *
41
+ * The typed data is `PermitWitnessTransferFrom` with `TabSettlement` as the
42
+ * witness struct, field for field what the contract rebuilds before it hands
43
+ * Permit2 the signature. Nothing is sent.
44
+ */
45
+ export async function signSettlementPermit(input) {
46
+ if (!isAddress(input.tabSettlement)) {
47
+ return validationError("SETTLEMENT_CONTRACT_INVALID", `\`${String(input.tabSettlement)}\` is not the address of a TabSettlement`);
48
+ }
49
+ if (!isAddress(input.asset))
50
+ return validationError("ASSET_REF_INVALID", "asset must be a 20-byte 0x address");
51
+ if (typeof input.amount !== "bigint" || input.amount <= 0n || input.amount >= 1n << 128n) {
52
+ return validationError("AMOUNT_OUT_OF_RANGE", "amount must be a positive bigint that fits a uint128");
53
+ }
54
+ const permit2 = (input.permit2 ?? PERMIT2_ADDRESS);
55
+ const chainId = BigInt(input.chainId);
56
+ const nonce = input.nonce ?? randomPermitNonce();
57
+ const now = input.now ?? (() => Date.now());
58
+ const deadline = input.deadline ?? BigInt(Math.floor(now() / 1000) + DEFAULT_PERMIT_TTL_SECONDS);
59
+ const tabSettlement = getAddress(input.tabSettlement);
60
+ const asset = getAddress(input.asset);
61
+ const agent = await wrap(async () => getAddress(await input.signer.getAddress()), (error) => ({
62
+ category: "UPSTREAM",
63
+ code: "SIGNER_ADDRESS_UNAVAILABLE",
64
+ message: "the Agent's address could not be read from its signer",
65
+ retryable: true,
66
+ cause: causeOf(error),
67
+ }));
68
+ if (!agent.ok)
69
+ return agent;
70
+ const signature = await wrap(async () => input.signer.signTypedData(permit2Domain(chainId, permit2), PERMIT2_WITNESS_TRANSFER_FROM_TYPES, {
71
+ permitted: { token: asset, amount: input.amount },
72
+ spender: tabSettlement,
73
+ nonce,
74
+ deadline,
75
+ witness: { serviceId: input.serviceId, asset, amount: input.amount, surface: tabSettlement, chainId },
76
+ }), (error) => ({
77
+ category: "UPSTREAM",
78
+ code: "PERMIT_SIGNATURE_REFUSED",
79
+ message: "the signer did not produce a Permit2 signature",
80
+ retryable: false,
81
+ cause: causeOf(error),
82
+ }));
83
+ if (!signature.ok)
84
+ return signature;
85
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature.value)) {
86
+ return chainError("PERMIT_SIGNATURE_MALFORMED", "the signer returned something that is not a 65-byte signature");
87
+ }
88
+ return ok({
89
+ agent: agent.value,
90
+ serviceId: input.serviceId,
91
+ asset,
92
+ amount: input.amount,
93
+ nonce,
94
+ deadline,
95
+ signature: signature.value,
96
+ chainId,
97
+ tabSettlement,
98
+ permit2,
99
+ });
100
+ }
101
+ /** Calldata for `settleWithPermit2`, for a relayer that submits it itself. */
102
+ export function encodeSettleWithPermit2(permit) {
103
+ return permitInterface.encodeFunctionData("settleWithPermit2", [
104
+ permit.agent,
105
+ permit.serviceId,
106
+ permit.asset,
107
+ permit.amount,
108
+ permit.nonce,
109
+ permit.deadline,
110
+ permit.signature,
111
+ ]);
112
+ }
113
+ export function toRelayBody(permit) {
114
+ return {
115
+ agent: permit.agent,
116
+ serviceId: permit.serviceId,
117
+ asset: permit.asset,
118
+ amount: permit.amount.toString(10),
119
+ nonce: permit.nonce.toString(10),
120
+ deadline: permit.deadline.toString(10),
121
+ signature: permit.signature,
122
+ chainId: permit.chainId.toString(10),
123
+ tabSettlement: permit.tabSettlement,
124
+ };
125
+ }
126
+ /** Reads a relay body back into a permit, refusing anything that is not one. */
127
+ export function fromRelayBody(value, permit2 = PERMIT2_ADDRESS) {
128
+ if (typeof value !== "object" || value === null) {
129
+ return validationError("RELAY_BODY_INVALID", "the relay body must be a JSON object");
130
+ }
131
+ const body = value;
132
+ const digits = (field) => {
133
+ const raw = body[field];
134
+ if (typeof raw !== "string" || !/^[0-9]+$/.test(raw)) {
135
+ return validationError("RELAY_BODY_INVALID", `${field} must be a decimal string`, { details: { field } });
136
+ }
137
+ return ok(BigInt(raw));
138
+ };
139
+ if (!isAddress(body.agent) || !isAddress(body.asset) || !isAddress(body.tabSettlement)) {
140
+ return validationError("RELAY_BODY_INVALID", "agent, asset and tabSettlement must be 20-byte 0x addresses");
141
+ }
142
+ if (typeof body.serviceId !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(body.serviceId)) {
143
+ return validationError("RELAY_BODY_INVALID", "serviceId must be a 32-byte 0x word");
144
+ }
145
+ if (typeof body.signature !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(body.signature)) {
146
+ return validationError("RELAY_BODY_INVALID", "signature must be a 65-byte 0x hex string");
147
+ }
148
+ const amount = digits("amount");
149
+ if (!amount.ok)
150
+ return amount;
151
+ if (amount.value <= 0n || amount.value >= 1n << 128n) {
152
+ return validationError("RELAY_BODY_INVALID", "amount must be a positive uint128");
153
+ }
154
+ const nonce = digits("nonce");
155
+ if (!nonce.ok)
156
+ return nonce;
157
+ const deadline = digits("deadline");
158
+ if (!deadline.ok)
159
+ return deadline;
160
+ const chainId = digits("chainId");
161
+ if (!chainId.ok)
162
+ return chainId;
163
+ return ok({
164
+ agent: getAddress(body.agent),
165
+ serviceId: body.serviceId.toLowerCase(),
166
+ asset: getAddress(body.asset),
167
+ amount: amount.value,
168
+ nonce: nonce.value,
169
+ deadline: deadline.value,
170
+ signature: body.signature,
171
+ chainId: chainId.value,
172
+ tabSettlement: getAddress(body.tabSettlement),
173
+ permit2,
174
+ });
175
+ }
176
+ function hostFetch() {
177
+ const candidate = globalThis.fetch;
178
+ return typeof candidate === "function" ? candidate : undefined;
179
+ }
180
+ /**
181
+ * A strategy that settles by signature and a relay, never by a transaction of
182
+ * the Agent's own.
183
+ */
184
+ export function createRelayedMonadStrategy(config) {
185
+ const logger = config.logger ?? defaultLogger;
186
+ const id = config.id ?? RELAYED_MONAD_STRATEGY_ID;
187
+ const now = config.now ?? (() => Date.now());
188
+ const allowanceMode = config.allowance ?? "require";
189
+ const permit2 = (config.permit2 ?? PERMIT2_ADDRESS);
190
+ const assets = indexAssets(config.assets, id, logger);
191
+ const chainIds = [...new Set([...assets.values()].map((asset) => asset.chainId))].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
192
+ const requireKnown = (asset) => {
193
+ const shape = validateAssetRef(asset, "request.asset");
194
+ if (!shape.ok)
195
+ return shape;
196
+ const found = assets.get(assetKey(shape.value));
197
+ if (found === undefined) {
198
+ return validationError("ASSET_NOT_CONFIGURED", `strategy \`${id}\` is not configured for asset ${assetKey(shape.value)}`, {
199
+ details: { asset: assetKey(shape.value), configured: [...assets.keys()].join(", ") },
200
+ });
201
+ }
202
+ return ok(found);
203
+ };
204
+ const ensurePermit2Allowance = async (asset, owner, needed) => {
205
+ if (allowanceMode === "skip")
206
+ return ok(undefined);
207
+ const provider = config.signer.provider;
208
+ if (provider === undefined || provider === null) {
209
+ return upstreamError("PROVIDER_REQUIRED", `strategy \`${id}\` needs a connected signer to read the Permit2 allowance; connect one or set allowance to "skip"`);
210
+ }
211
+ const current = await wrap(async () => {
212
+ const raw = await provider.call({ to: asset.address, data: erc20.encodeFunctionData("allowance", [owner, permit2]) });
213
+ return erc20.decodeFunctionResult("allowance", raw)[0];
214
+ }, (error) => ({
215
+ category: "UPSTREAM",
216
+ code: "ALLOWANCE_READ_FAILED",
217
+ message: `strategy \`${id}\` could not read the Permit2 allowance on ${assetKey(asset)}`,
218
+ retryable: true,
219
+ cause: causeOf(error),
220
+ }));
221
+ if (!current.ok)
222
+ return current;
223
+ if (current.value >= needed)
224
+ return ok(undefined);
225
+ if (allowanceMode === "require" || typeof config.signer.sendTransaction !== "function") {
226
+ return validationError("PERMIT2_ALLOWANCE_MISSING", `the Agent has not approved Permit2 (${permit2}) on ${assetKey(asset)}; approve it once, with gas, and every Settlement after that is a signature`, { details: { asset: assetKey(asset), permit2, allowance: current.value.toString(10), needed: needed.toString(10) } });
227
+ }
228
+ const sent = await wrap(async () => config.signer.sendTransaction({ to: asset.address, data: erc20.encodeFunctionData("approve", [permit2, MaxUint256]) }), (error) => ({
229
+ category: "CHAIN",
230
+ code: "SETTLEMENT_SUBMISSION_FAILED",
231
+ message: `strategy \`${id}\` could not submit the one-time Permit2 approval on ${assetKey(asset)}`,
232
+ retryable: true,
233
+ cause: causeOf(error),
234
+ }));
235
+ if (!sent.ok)
236
+ return sent;
237
+ if (typeof sent.value.wait === "function") {
238
+ const landed = await wrap(async () => sent.value.wait?.() ?? null, (error) => ({
239
+ category: "CHAIN",
240
+ code: "APPROVAL_RECEIPT_UNAVAILABLE",
241
+ message: `strategy \`${id}\` could not confirm the Permit2 approval ${sent.value.hash}`,
242
+ retryable: true,
243
+ cause: causeOf(error),
244
+ }));
245
+ if (!landed.ok)
246
+ return landed;
247
+ }
248
+ logger.info("Permit2 approved once for gasless settlement", { strategyId: id, asset: assetKey(asset), txHash: sent.value.hash });
249
+ return ok(undefined);
250
+ };
251
+ const relay = async (permit) => {
252
+ const send = config.fetchImpl ?? hostFetch();
253
+ if (send === undefined)
254
+ return upstreamError("FETCH_UNAVAILABLE", "this host has no global fetch, so the relay cannot be reached");
255
+ const response = await wrap(async () => send(config.relayUrl, {
256
+ method: "POST",
257
+ headers: { "content-type": "application/json" },
258
+ body: JSON.stringify(toRelayBody(permit)),
259
+ }), (error) => ({
260
+ category: "UPSTREAM",
261
+ code: "RELAY_UNREACHABLE",
262
+ message: `the relay at ${config.relayUrl} did not answer`,
263
+ retryable: true,
264
+ cause: causeOf(error),
265
+ }));
266
+ if (!response.ok)
267
+ return response;
268
+ const body = await wrap(async () => response.value.json(), (error) => ({
269
+ category: "UPSTREAM",
270
+ code: "RELAY_UNPARSEABLE",
271
+ message: "the relay answered with something that is not JSON",
272
+ retryable: true,
273
+ cause: causeOf(error),
274
+ }));
275
+ if (!body.ok)
276
+ return body;
277
+ const reply = body.value;
278
+ if (response.value.status >= 400 || reply.ok !== true || typeof reply.txHash !== "string") {
279
+ return {
280
+ ok: false,
281
+ error: {
282
+ category: response.value.status >= 500 ? "UPSTREAM" : "CHAIN",
283
+ code: reply.error?.code ?? "RELAY_REFUSED",
284
+ message: reply.error?.message ?? `the relay answered ${response.value.status} without a transaction hash`,
285
+ retryable: response.value.status >= 500,
286
+ },
287
+ };
288
+ }
289
+ return ok({
290
+ ok: true,
291
+ txHash: reply.txHash,
292
+ settlementId: typeof reply.settlementId === "string" ? reply.settlementId : null,
293
+ applied: typeof reply.applied === "string" ? reply.applied : null,
294
+ toPrepaid: typeof reply.toPrepaid === "string" ? reply.toPrepaid : null,
295
+ });
296
+ };
297
+ return {
298
+ id,
299
+ chainIds,
300
+ supports(asset) {
301
+ return assets.get(assetKey(asset)) !== undefined;
302
+ },
303
+ async quote(request) {
304
+ const asset = requireKnown(request.asset);
305
+ if (!asset.ok)
306
+ return asset;
307
+ if (typeof request.amount !== "bigint" || request.amount <= 0n) {
308
+ return validationError("AMOUNT_NOT_POSITIVE", "request.amount must be a positive bigint of base units");
309
+ }
310
+ return ok({
311
+ amount: request.amount,
312
+ asset: asset.value,
313
+ feeNote: "The amount charged is the amount transferred. The Agent signs and the relay pays the gas, so nothing in MON leaves the Agent's account.",
314
+ });
315
+ },
316
+ async settle(request) {
317
+ const validated = validateSettleRequest(request);
318
+ if (!validated.ok)
319
+ return validated;
320
+ const asset = requireKnown(request.asset);
321
+ if (!asset.ok)
322
+ return asset;
323
+ const owner = await wrap(async () => getAddress(await config.signer.getAddress()), (error) => ({
324
+ category: "UPSTREAM",
325
+ code: "SIGNER_ADDRESS_UNAVAILABLE",
326
+ message: `strategy \`${id}\` could not read the payer address from its signer`,
327
+ retryable: true,
328
+ cause: causeOf(error),
329
+ }));
330
+ if (!owner.ok)
331
+ return owner;
332
+ const allowance = await ensurePermit2Allowance(asset.value, owner.value, request.amount);
333
+ if (!allowance.ok)
334
+ return allowance;
335
+ const permit = await signSettlementPermit({
336
+ signer: config.signer,
337
+ chainId: asset.value.chainId,
338
+ tabSettlement: config.tabSettlement,
339
+ serviceId: request.serviceId,
340
+ asset: asset.value.address,
341
+ amount: request.amount,
342
+ permit2,
343
+ now,
344
+ });
345
+ if (!permit.ok)
346
+ return permit;
347
+ const relayed = await relay(permit.value);
348
+ if (!relayed.ok)
349
+ return relayed;
350
+ return ok({
351
+ strategyId: id,
352
+ chainId: asset.value.chainId,
353
+ txHash: relayed.value.txHash,
354
+ asset: asset.value,
355
+ amount: request.amount,
356
+ payer: owner.value,
357
+ serviceId: request.serviceId,
358
+ settlementId: relayed.value.settlementId === null ? null : relayed.value.settlementId.toLowerCase(),
359
+ applied: relayed.value.applied === null ? null : BigInt(relayed.value.applied),
360
+ toPrepaid: relayed.value.toPrepaid === null ? null : BigInt(relayed.value.toPrepaid),
361
+ submittedAt: now(),
362
+ });
363
+ },
364
+ };
365
+ }
366
+ //# sourceMappingURL=permit2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permit2.js","sourceRoot":"","sources":["../../src/payments/permit2.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Gasless Settlement: the Agent signs, somebody else sends.\n *\n * `TabSettlement.settleWithPermit2` takes a Permit2 `PermitWitnessTransferFrom`\n * signature from the Agent and lets any account submit it. The witness binds\n * the Service, the Asset, the amount, the settlement surface and the chain, so\n * a relayer cannot redirect or resize what the Agent authorised; the nonce\n * stops a replay; the deadline bounds how long the signature is good for. What\n * the relayer contributes is gas, and the natural relayer is the Service's own\n * gateway, which wants to be paid.\n *\n * Two pieces live here. {@link signSettlementPermit} produces the signature\n * and the exact arguments the contract takes, for any code that holds an\n * Agent's signer. {@link createRelayedMonadStrategy} is a `PaymentStrategy`\n * that signs and posts to a relay endpoint, so `tab_settle` needs no MON in\n * the Agent's account at all once the one-time Permit2 approval is in place.\n *\n * The one prerequisite is that approval: Permit2 pulls the Asset with\n * `transferFrom`, so the Agent must have approved Permit2 once on the Asset.\n * That is one gas-paid transaction per Asset for the life of the account, and\n * the strategy says so by name when it is missing rather than posting a\n * signature the relayer would only watch revert.\n */\nimport { Interface, MaxUint256, getAddress, hexlify, randomBytes } from \"ethers\";\nimport {\n PERMIT2_ADDRESS,\n PERMIT2_WITNESS_TRANSFER_FROM_PRIMARY_TYPE,\n PERMIT2_WITNESS_TRANSFER_FROM_TYPES,\n TAB_SETTLEMENT_PERMIT2_ABI,\n causeOf,\n isAddress,\n ok,\n permit2Domain,\n wrap,\n type Address,\n type Bytes32,\n type Hex,\n type Result,\n} from \"../_shared/index.js\";\nimport { defaultLogger, type Logger } from \"../logger.js\";\nimport { chainError, upstreamError, validationError } from \"../errors.js\";\nimport { ERC20_ABI } from \"./abi.js\";\nimport {\n indexAssets,\n type EthersV6CallProvider,\n type EthersV6Signer,\n type EthersV6TransactionRequest,\n type EthersV6TransactionResponse,\n} from \"./monad.js\";\nimport {\n assetKey,\n validateAssetRef,\n validateSettleRequest,\n type AssetRef,\n type ChargeQuote,\n type ChargeRequest,\n type PaymentStrategy,\n type SettleRequest,\n type SettlementReceipt,\n} from \"./strategy.js\";\n\nexport const RELAYED_MONAD_STRATEGY_ID = \"monad-relayed\";\n\n/** How long a signature stays valid when the caller names no deadline: ten minutes. */\nexport const DEFAULT_PERMIT_TTL_SECONDS = 600;\n\n/** A signer that can produce EIP-712 signatures, which an ethers `Signer` can. */\nexport interface Permit2Signer {\n getAddress(): Promise<string>;\n signTypedData(\n domain: { name: string; chainId: bigint; verifyingContract: string },\n types: Record<string, readonly { name: string; type: string }[]>,\n value: Record<string, unknown>,\n ): Promise<string>;\n readonly provider?: EthersV6CallProvider | null;\n sendTransaction?(transaction: EthersV6TransactionRequest): Promise<EthersV6TransactionResponse>;\n}\n\nexport interface SettlementPermitInput {\n readonly signer: Permit2Signer;\n readonly chainId: bigint | number;\n /** The deployed `TabSettlement`, which is the `spender` Permit2 sees. */\n readonly tabSettlement: Address;\n readonly serviceId: Bytes32;\n readonly asset: Address;\n readonly amount: bigint;\n /** The canonical Permit2 unless a deployment overrides it. */\n readonly permit2?: Address;\n /** A Permit2 unordered nonce. Random when omitted, which is what Permit2 is built for. */\n readonly nonce?: bigint;\n /** Unix seconds. Defaults to now plus {@link DEFAULT_PERMIT_TTL_SECONDS}. */\n readonly deadline?: bigint;\n readonly now?: () => number;\n}\n\n/** Exactly what `settleWithPermit2` takes, plus what was signed, for the record. */\nexport interface SettlementPermit {\n readonly agent: Address;\n readonly serviceId: Bytes32;\n readonly asset: Address;\n readonly amount: bigint;\n readonly nonce: bigint;\n readonly deadline: bigint;\n readonly signature: Hex;\n readonly chainId: bigint;\n readonly tabSettlement: Address;\n readonly permit2: Address;\n}\n\nconst permitInterface = new Interface(TAB_SETTLEMENT_PERMIT2_ABI);\nconst erc20 = new Interface(ERC20_ABI);\n\n/** A random 256-bit unordered nonce. Permit2 keys them per owner in a bitmap. */\nexport const randomPermitNonce = (): bigint => BigInt(hexlify(randomBytes(32)));\n\n/**\n * Signs one Settlement under Permit2's domain.\n *\n * The typed data is `PermitWitnessTransferFrom` with `TabSettlement` as the\n * witness struct, field for field what the contract rebuilds before it hands\n * Permit2 the signature. Nothing is sent.\n */\nexport async function signSettlementPermit(input: SettlementPermitInput): Promise<Result<SettlementPermit>> {\n if (!isAddress(input.tabSettlement)) {\n return validationError(\"SETTLEMENT_CONTRACT_INVALID\", `\\`${String(input.tabSettlement)}\\` is not the address of a TabSettlement`);\n }\n if (!isAddress(input.asset)) return validationError(\"ASSET_REF_INVALID\", \"asset must be a 20-byte 0x address\");\n if (typeof input.amount !== \"bigint\" || input.amount <= 0n || input.amount >= 1n << 128n) {\n return validationError(\"AMOUNT_OUT_OF_RANGE\", \"amount must be a positive bigint that fits a uint128\");\n }\n const permit2 = (input.permit2 ?? PERMIT2_ADDRESS) as Address;\n const chainId = BigInt(input.chainId);\n const nonce = input.nonce ?? randomPermitNonce();\n const now = input.now ?? (() => Date.now());\n const deadline = input.deadline ?? BigInt(Math.floor(now() / 1000) + DEFAULT_PERMIT_TTL_SECONDS);\n const tabSettlement = getAddress(input.tabSettlement) as Address;\n const asset = getAddress(input.asset) as Address;\n\n const agent = await wrap(\n async () => getAddress(await input.signer.getAddress()) as Address,\n (error) => ({\n category: \"UPSTREAM\",\n code: \"SIGNER_ADDRESS_UNAVAILABLE\",\n message: \"the Agent's address could not be read from its signer\",\n retryable: true,\n cause: causeOf(error),\n }),\n );\n if (!agent.ok) return agent;\n\n const signature = await wrap(\n async () =>\n input.signer.signTypedData(\n permit2Domain(chainId, permit2),\n PERMIT2_WITNESS_TRANSFER_FROM_TYPES as unknown as Record<string, readonly { name: string; type: string }[]>,\n {\n permitted: { token: asset, amount: input.amount },\n spender: tabSettlement,\n nonce,\n deadline,\n witness: { serviceId: input.serviceId, asset, amount: input.amount, surface: tabSettlement, chainId },\n },\n ),\n (error) => ({\n category: \"UPSTREAM\",\n code: \"PERMIT_SIGNATURE_REFUSED\",\n message: \"the signer did not produce a Permit2 signature\",\n retryable: false,\n cause: causeOf(error),\n }),\n );\n if (!signature.ok) return signature;\n if (!/^0x[0-9a-fA-F]{130}$/.test(signature.value)) {\n return chainError(\"PERMIT_SIGNATURE_MALFORMED\", \"the signer returned something that is not a 65-byte signature\");\n }\n return ok({\n agent: agent.value,\n serviceId: input.serviceId,\n asset,\n amount: input.amount,\n nonce,\n deadline,\n signature: signature.value as Hex,\n chainId,\n tabSettlement,\n permit2,\n });\n}\n\n/** Calldata for `settleWithPermit2`, for a relayer that submits it itself. */\nexport function encodeSettleWithPermit2(permit: SettlementPermit): Hex {\n return permitInterface.encodeFunctionData(\"settleWithPermit2\", [\n permit.agent,\n permit.serviceId,\n permit.asset,\n permit.amount,\n permit.nonce,\n permit.deadline,\n permit.signature,\n ]) as Hex;\n}\n\n/** The JSON body a relay endpoint takes: the permit with every integer as decimal text. */\nexport interface RelaySettleBody {\n readonly agent: string;\n readonly serviceId: string;\n readonly asset: string;\n readonly amount: string;\n readonly nonce: string;\n readonly deadline: string;\n readonly signature: string;\n readonly chainId: string;\n readonly tabSettlement: string;\n}\n\nexport function toRelayBody(permit: SettlementPermit): RelaySettleBody {\n return {\n agent: permit.agent,\n serviceId: permit.serviceId,\n asset: permit.asset,\n amount: permit.amount.toString(10),\n nonce: permit.nonce.toString(10),\n deadline: permit.deadline.toString(10),\n signature: permit.signature,\n chainId: permit.chainId.toString(10),\n tabSettlement: permit.tabSettlement,\n };\n}\n\n/** Reads a relay body back into a permit, refusing anything that is not one. */\nexport function fromRelayBody(value: unknown, permit2: Address = PERMIT2_ADDRESS as Address): Result<SettlementPermit> {\n if (typeof value !== \"object\" || value === null) {\n return validationError(\"RELAY_BODY_INVALID\", \"the relay body must be a JSON object\");\n }\n const body = value as Partial<RelaySettleBody>;\n const digits = (field: keyof RelaySettleBody): Result<bigint> => {\n const raw = body[field];\n if (typeof raw !== \"string\" || !/^[0-9]+$/.test(raw)) {\n return validationError(\"RELAY_BODY_INVALID\", `${field} must be a decimal string`, { details: { field } });\n }\n return ok(BigInt(raw));\n };\n if (!isAddress(body.agent) || !isAddress(body.asset) || !isAddress(body.tabSettlement)) {\n return validationError(\"RELAY_BODY_INVALID\", \"agent, asset and tabSettlement must be 20-byte 0x addresses\");\n }\n if (typeof body.serviceId !== \"string\" || !/^0x[0-9a-fA-F]{64}$/.test(body.serviceId)) {\n return validationError(\"RELAY_BODY_INVALID\", \"serviceId must be a 32-byte 0x word\");\n }\n if (typeof body.signature !== \"string\" || !/^0x[0-9a-fA-F]{130}$/.test(body.signature)) {\n return validationError(\"RELAY_BODY_INVALID\", \"signature must be a 65-byte 0x hex string\");\n }\n const amount = digits(\"amount\");\n if (!amount.ok) return amount;\n if (amount.value <= 0n || amount.value >= 1n << 128n) {\n return validationError(\"RELAY_BODY_INVALID\", \"amount must be a positive uint128\");\n }\n const nonce = digits(\"nonce\");\n if (!nonce.ok) return nonce;\n const deadline = digits(\"deadline\");\n if (!deadline.ok) return deadline;\n const chainId = digits(\"chainId\");\n if (!chainId.ok) return chainId;\n return ok({\n agent: getAddress(body.agent) as Address,\n serviceId: body.serviceId.toLowerCase() as Bytes32,\n asset: getAddress(body.asset) as Address,\n amount: amount.value,\n nonce: nonce.value,\n deadline: deadline.value,\n signature: body.signature as Hex,\n chainId: chainId.value,\n tabSettlement: getAddress(body.tabSettlement) as Address,\n permit2,\n });\n}\n\n/** What a relay answers once the Settlement is mined. */\nexport interface RelaySettleReply {\n readonly ok: true;\n readonly txHash: string;\n readonly settlementId: string | null;\n readonly applied: string | null;\n readonly toPrepaid: string | null;\n}\n\n/** The little of `fetch` the strategy uses. */\nexport type RelayFetch = (\n url: string,\n init: { method: string; headers: Record<string, string>; body: string },\n) => Promise<{ readonly status: number; json(): Promise<unknown> }>;\n\nexport interface RelayedMonadStrategyConfig {\n readonly signer: Permit2Signer;\n readonly tabSettlement: Address;\n /** The relay endpoint, for example `https://gateway.example/relay/settle`. */\n readonly relayUrl: string;\n readonly assets: Readonly<Record<string, AssetRef>>;\n readonly permit2?: Address;\n readonly id?: string;\n /**\n * What to do when the Agent has not approved Permit2 on the Asset. `require`\n * refuses with `PERMIT2_ALLOWANCE_MISSING`; `approve` sends the one-time\n * unlimited approval first, which needs MON once; `skip` does not read it.\n */\n readonly allowance?: \"require\" | \"approve\" | \"skip\";\n readonly fetchImpl?: RelayFetch;\n readonly logger?: Logger;\n readonly now?: () => number;\n}\n\nfunction hostFetch(): RelayFetch | undefined {\n const candidate = (globalThis as { fetch?: unknown }).fetch;\n return typeof candidate === \"function\" ? (candidate as RelayFetch) : undefined;\n}\n\n/**\n * A strategy that settles by signature and a relay, never by a transaction of\n * the Agent's own.\n */\nexport function createRelayedMonadStrategy(config: RelayedMonadStrategyConfig): PaymentStrategy {\n const logger = config.logger ?? defaultLogger;\n const id = config.id ?? RELAYED_MONAD_STRATEGY_ID;\n const now = config.now ?? (() => Date.now());\n const allowanceMode = config.allowance ?? \"require\";\n const permit2 = (config.permit2 ?? PERMIT2_ADDRESS) as Address;\n const assets = indexAssets(config.assets, id, logger);\n const chainIds = [...new Set([...assets.values()].map((asset) => asset.chainId))].sort((left, right) =>\n left < right ? -1 : left > right ? 1 : 0,\n );\n\n const requireKnown = (asset: AssetRef): Result<AssetRef> => {\n const shape = validateAssetRef(asset, \"request.asset\");\n if (!shape.ok) return shape;\n const found = assets.get(assetKey(shape.value));\n if (found === undefined) {\n return validationError(\"ASSET_NOT_CONFIGURED\", `strategy \\`${id}\\` is not configured for asset ${assetKey(shape.value)}`, {\n details: { asset: assetKey(shape.value), configured: [...assets.keys()].join(\", \") },\n });\n }\n return ok(found);\n };\n\n const ensurePermit2Allowance = async (asset: AssetRef, owner: Address, needed: bigint): Promise<Result<void>> => {\n if (allowanceMode === \"skip\") return ok(undefined);\n const provider = config.signer.provider;\n if (provider === undefined || provider === null) {\n return upstreamError(\n \"PROVIDER_REQUIRED\",\n `strategy \\`${id}\\` needs a connected signer to read the Permit2 allowance; connect one or set allowance to \"skip\"`,\n );\n }\n const current = await wrap(\n async () => {\n const raw = await provider.call({ to: asset.address, data: erc20.encodeFunctionData(\"allowance\", [owner, permit2]) });\n return erc20.decodeFunctionResult(\"allowance\", raw)[0] as bigint;\n },\n (error) => ({\n category: \"UPSTREAM\",\n code: \"ALLOWANCE_READ_FAILED\",\n message: `strategy \\`${id}\\` could not read the Permit2 allowance on ${assetKey(asset)}`,\n retryable: true,\n cause: causeOf(error),\n }),\n );\n if (!current.ok) return current;\n if (current.value >= needed) return ok(undefined);\n if (allowanceMode === \"require\" || typeof config.signer.sendTransaction !== \"function\") {\n return validationError(\n \"PERMIT2_ALLOWANCE_MISSING\",\n `the Agent has not approved Permit2 (${permit2}) on ${assetKey(asset)}; approve it once, with gas, and every Settlement after that is a signature`,\n { details: { asset: assetKey(asset), permit2, allowance: current.value.toString(10), needed: needed.toString(10) } },\n );\n }\n const sent = await wrap(\n async () => config.signer.sendTransaction!({ to: asset.address, data: erc20.encodeFunctionData(\"approve\", [permit2, MaxUint256]) }),\n (error) => ({\n category: \"CHAIN\",\n code: \"SETTLEMENT_SUBMISSION_FAILED\",\n message: `strategy \\`${id}\\` could not submit the one-time Permit2 approval on ${assetKey(asset)}`,\n retryable: true,\n cause: causeOf(error),\n }),\n );\n if (!sent.ok) return sent;\n if (typeof sent.value.wait === \"function\") {\n const landed = await wrap(\n async () => sent.value.wait?.() ?? null,\n (error) => ({\n category: \"CHAIN\",\n code: \"APPROVAL_RECEIPT_UNAVAILABLE\",\n message: `strategy \\`${id}\\` could not confirm the Permit2 approval ${sent.value.hash}`,\n retryable: true,\n cause: causeOf(error),\n }),\n );\n if (!landed.ok) return landed;\n }\n logger.info(\"Permit2 approved once for gasless settlement\", { strategyId: id, asset: assetKey(asset), txHash: sent.value.hash });\n return ok(undefined);\n };\n\n const relay = async (permit: SettlementPermit): Promise<Result<RelaySettleReply>> => {\n const send = config.fetchImpl ?? hostFetch();\n if (send === undefined) return upstreamError(\"FETCH_UNAVAILABLE\", \"this host has no global fetch, so the relay cannot be reached\");\n const response = await wrap(\n async () =>\n send(config.relayUrl, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(toRelayBody(permit)),\n }),\n (error) => ({\n category: \"UPSTREAM\",\n code: \"RELAY_UNREACHABLE\",\n message: `the relay at ${config.relayUrl} did not answer`,\n retryable: true,\n cause: causeOf(error),\n }),\n );\n if (!response.ok) return response;\n const body = await wrap(\n async () => response.value.json(),\n (error) => ({\n category: \"UPSTREAM\",\n code: \"RELAY_UNPARSEABLE\",\n message: \"the relay answered with something that is not JSON\",\n retryable: true,\n cause: causeOf(error),\n }),\n );\n if (!body.ok) return body;\n const reply = body.value as Partial<RelaySettleReply> & { error?: { code?: string; message?: string; category?: string } };\n if (response.value.status >= 400 || reply.ok !== true || typeof reply.txHash !== \"string\") {\n return {\n ok: false,\n error: {\n category: response.value.status >= 500 ? \"UPSTREAM\" : \"CHAIN\",\n code: reply.error?.code ?? \"RELAY_REFUSED\",\n message: reply.error?.message ?? `the relay answered ${response.value.status} without a transaction hash`,\n retryable: response.value.status >= 500,\n },\n };\n }\n return ok({\n ok: true,\n txHash: reply.txHash,\n settlementId: typeof reply.settlementId === \"string\" ? reply.settlementId : null,\n applied: typeof reply.applied === \"string\" ? reply.applied : null,\n toPrepaid: typeof reply.toPrepaid === \"string\" ? reply.toPrepaid : null,\n });\n };\n\n return {\n id,\n chainIds,\n supports(asset) {\n return assets.get(assetKey(asset)) !== undefined;\n },\n async quote(request: ChargeRequest): Promise<Result<ChargeQuote>> {\n const asset = requireKnown(request.asset);\n if (!asset.ok) return asset;\n if (typeof request.amount !== \"bigint\" || request.amount <= 0n) {\n return validationError(\"AMOUNT_NOT_POSITIVE\", \"request.amount must be a positive bigint of base units\");\n }\n return ok({\n amount: request.amount,\n asset: asset.value,\n feeNote:\n \"The amount charged is the amount transferred. The Agent signs and the relay pays the gas, so nothing in MON leaves the Agent's account.\",\n });\n },\n async settle(request: SettleRequest): Promise<Result<SettlementReceipt>> {\n const validated = validateSettleRequest(request);\n if (!validated.ok) return validated;\n const asset = requireKnown(request.asset);\n if (!asset.ok) return asset;\n const owner = await wrap(\n async () => getAddress(await config.signer.getAddress()) as Address,\n (error) => ({\n category: \"UPSTREAM\",\n code: \"SIGNER_ADDRESS_UNAVAILABLE\",\n message: `strategy \\`${id}\\` could not read the payer address from its signer`,\n retryable: true,\n cause: causeOf(error),\n }),\n );\n if (!owner.ok) return owner;\n const allowance = await ensurePermit2Allowance(asset.value, owner.value, request.amount);\n if (!allowance.ok) return allowance;\n const permit = await signSettlementPermit({\n signer: config.signer,\n chainId: asset.value.chainId,\n tabSettlement: config.tabSettlement,\n serviceId: request.serviceId,\n asset: asset.value.address,\n amount: request.amount,\n permit2,\n now,\n });\n if (!permit.ok) return permit;\n const relayed = await relay(permit.value);\n if (!relayed.ok) return relayed;\n return ok({\n strategyId: id,\n chainId: asset.value.chainId,\n txHash: relayed.value.txHash as Hex,\n asset: asset.value,\n amount: request.amount,\n payer: owner.value,\n serviceId: request.serviceId,\n settlementId: relayed.value.settlementId === null ? null : (relayed.value.settlementId.toLowerCase() as Bytes32),\n applied: relayed.value.applied === null ? null : BigInt(relayed.value.applied),\n toPrepaid: relayed.value.toPrepaid === null ? null : BigInt(relayed.value.toPrepaid),\n submittedAt: now(),\n });\n },\n };\n}\n"]}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Strategy registration and resolution: mechanisms one and two of R23.6.
3
+ *
4
+ * One implementation serves both. A registry is a keyed set of strategies with an
5
+ * optional parent it falls back to:
6
+ *
7
+ * - **Constructor injection.** `createStrategyRegistry({ strategies: [...] })`
8
+ * is what a client wrapper builds from its own options, so a consumer passes
9
+ * strategies in and never touches global state. Its parent is the module-level
10
+ * registry by default, so injection adds to what is already registered instead
11
+ * of hiding it.
12
+ * - **The module-level registry.** `registerPaymentStrategy(strategy)` writes to
13
+ * one process-wide registry, which is what a plugin package needs: it
14
+ * self-registers on import and the consumer's only line is the import.
15
+ *
16
+ * ## Why the module-level state hangs off `globalThis`
17
+ *
18
+ * A plugin package that self-registers on import may resolve a *different copy*
19
+ * of `@tabai/sdk` than the application does, different versions, a nested
20
+ * `node_modules`, a bundled duplicate. With module-scoped state each copy gets
21
+ * its own registry, the plugin registers into a registry the application never
22
+ * reads, and the failure is silent: the strategy simply never resolves. So the
23
+ * state lives under a versioned `Symbol.for` key on `globalThis` and every copy
24
+ * of this module wraps the same `Map`. Only the shared state is global; the
25
+ * behaviour is each copy's own.
26
+ *
27
+ * ## Idempotence, and what replacement means
28
+ *
29
+ * `register` is keyed by `strategy.id`:
30
+ *
31
+ * - the same object again is a no-op, `unchanged`, no warning, no reordering;
32
+ * - a *different* object under a live id replaces it **in its original
33
+ * position** and warns through the logger rather than throwing (design section
34
+ * 9.3), because a strategy replaced at the back of the queue would silently
35
+ * change which strategy resolves for every other Asset too;
36
+ * - anything else appends.
37
+ *
38
+ * Order matters because resolution order is registration order, so the operation
39
+ * has to be idempotent in the ordering as well as in the membership. `Map.set`
40
+ * on a live key keeps the key's insertion position, which is exactly this rule.
41
+ *
42
+ * ## Resolution order
43
+ *
44
+ * An explicit `strategyId` wins. Otherwise the first strategy whose
45
+ * `supports(asset)` returns true, in registration order, own entries before the
46
+ * parent's (design section 9.3). A named strategy that does not support the Asset
47
+ * is an error rather than a silent fall-through to another one: a caller that
48
+ * named a strategy meant it.
49
+ *
50
+ * Requirements: 23.6, 21.5
51
+ */
52
+ import { type Result } from "../_shared/index.js";
53
+ import { type Logger } from "../logger.js";
54
+ import { type AssetRef, type PaymentStrategy } from "./strategy.js";
55
+ /** What `register` did. `unchanged` is the idempotent case. */
56
+ export type RegistrationAction = "registered" | "replaced" | "unchanged";
57
+ export interface StrategyRegistration {
58
+ readonly action: RegistrationAction;
59
+ readonly strategy: PaymentStrategy;
60
+ /** The strategy that was displaced, on `replaced` alone. */
61
+ readonly previous?: PaymentStrategy;
62
+ }
63
+ /** What resolution is given: the Asset that must be settled, and an optional override. */
64
+ export interface StrategyQuery {
65
+ readonly asset: AssetRef;
66
+ readonly strategyId?: string;
67
+ }
68
+ export interface StrategyRegistry {
69
+ register(strategy: PaymentStrategy): Result<StrategyRegistration>;
70
+ /** Removes a strategy from *this* registry. Never reaches into the parent. */
71
+ unregister(id: string): boolean;
72
+ /** Empties *this* registry. Never reaches into the parent. */
73
+ clear(): void;
74
+ /** This registry's own strategies in registration order, then the parent's. */
75
+ list(): readonly PaymentStrategy[];
76
+ get(id: string): PaymentStrategy | undefined;
77
+ resolve(query: StrategyQuery): Result<PaymentStrategy>;
78
+ }
79
+ export interface StrategyRegistryOptions {
80
+ /** Registered in array order at construction. Mechanism one. */
81
+ readonly strategies?: readonly PaymentStrategy[];
82
+ readonly logger?: Logger;
83
+ /**
84
+ * The registry to fall back to. Omitted means the module-level registry;
85
+ * `false` means an isolated registry, which is what the module-level registry
86
+ * itself is and what a test wants.
87
+ */
88
+ readonly inherit?: StrategyRegistry | false;
89
+ }
90
+ /**
91
+ * Builds a registry.
92
+ *
93
+ * Construction cannot fail, so it does not return a `Result`. An invalid entry in
94
+ * `strategies` is refused and reported through the logger, and the registry comes
95
+ * back holding the valid ones, a consumer's typo in one strategy should not take
96
+ * out the strategies that were fine. Use {@link StrategyRegistry.register}
97
+ * directly when the caller wants the `Result` for each one.
98
+ */
99
+ export declare function createStrategyRegistry(options?: StrategyRegistryOptions): StrategyRegistry;
100
+ /** The process-wide registry mechanism two writes to. */
101
+ export declare function moduleStrategyRegistry(logger?: Logger): StrategyRegistry;
102
+ /**
103
+ * Registers a strategy process-wide. Idempotent by `strategy.id`.
104
+ *
105
+ * This is the whole of mechanism two: a plugin package calls it at import time,
106
+ * and the consumer's only line is `import "@acme/tab-strategy-solana"`.
107
+ */
108
+ export declare function registerPaymentStrategy(strategy: PaymentStrategy, options?: {
109
+ readonly logger?: Logger;
110
+ }): Result<StrategyRegistration>;
111
+ /** Removes a process-wide registration. Returns false when the id was not registered. */
112
+ export declare const unregisterPaymentStrategy: (id: string) => boolean;
113
+ /** Every process-wide registration, in registration order. */
114
+ export declare const listPaymentStrategies: () => readonly PaymentStrategy[];
115
+ /** Empties the process-wide registry. Present for hosts that rebuild their world. */
116
+ export declare const clearPaymentStrategies: () => void;
117
+ /** Resolves against the process-wide registry alone. */
118
+ export declare const resolvePaymentStrategy: (query: StrategyQuery) => Result<PaymentStrategy>;
119
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/payments/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,EAAM,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,EAIL,KAAK,QAAQ,EACb,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AAEvB,+DAA+D;AAC/D,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,UAAU,GAAG,WAAW,CAAC;AAEzE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;CACrC;AAED,0FAA0F;AAC1F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAClE,8EAA8E;IAC9E,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,8DAA8D;IAC9D,KAAK,IAAI,IAAI,CAAC;IACd,+EAA+E;IAC/E,IAAI,IAAI,SAAS,eAAe,EAAE,CAAC;IACnC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAC7C,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,uBAAuB;IACtC,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,KAAK,CAAC;CAC7C;AAmBD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,gBAAgB,CAe9F;AAED,yDAAyD;AACzD,wBAAgB,sBAAsB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAExE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,eAAe,EACzB,OAAO,GAAE;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACzC,MAAM,CAAC,oBAAoB,CAAC,CAE9B;AAED,yFAAyF;AACzF,eAAO,MAAM,yBAAyB,GAAI,IAAI,MAAM,KAAG,OACd,CAAC;AAE1C,8DAA8D;AAC9D,eAAO,MAAM,qBAAqB,QAAO,SAAS,eAAe,EAChC,CAAC;AAElC,qFAAqF;AACrF,eAAO,MAAM,sBAAsB,QAAO,IAAwC,CAAC;AAEnF,wDAAwD;AACxD,eAAO,MAAM,sBAAsB,GAAI,OAAO,aAAa,KAAG,MAAM,CAAC,eAAe,CAC3C,CAAC"}