@nuanu-ai/agentify-contracts 0.3.2

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.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * The address money can be sent to on an EVM chain, and the one thing that can
3
+ * be checked about one before it is used.
4
+ *
5
+ * An address is twenty bytes and nothing about those bytes is redundant, so a
6
+ * character lost or mistyped in one produces another perfectly valid address —
7
+ * belonging to nobody, or to somebody else, and a payment to it is gone. EIP-55
8
+ * is the only guard anybody has against that: the capital letters in the
9
+ * spelling a wallet shows are not decoration, they are a checksum over the
10
+ * address itself, and a character that is wrong stops the capitals agreeing
11
+ * with the digits. Checked here, a mistyped address is a sentence a merchant
12
+ * reads while they still have the right one in front of them; not checked, it
13
+ * is a stranger's wallet filling up with somebody's sales.
14
+ *
15
+ * Two spellings are accepted and no others. All lower case is the spelling that
16
+ * carries no checksum at all, and it is what a block explorer prints and what
17
+ * half the tooling in this world hands somebody, so refusing it would refuse
18
+ * addresses that are perfectly good. The other is the exact output of the
19
+ * checksum, which is what a wallet puts on the clipboard. Anything between the
20
+ * two is a string that has been through something: a case-mangling copy, a
21
+ * document that shouted it, or a keystroke in the wrong place. The refusal says
22
+ * so and says what to do instead, because "invalid address" leaves a person
23
+ * staring at forty characters that look fine.
24
+ *
25
+ * Of the two, the wallet's is the canon: the door takes both spellings and
26
+ * everything behind it holds one (ADR-0017), and `checksummedAddressOf` below
27
+ * is what writes it. The reason is about the person rather than about the
28
+ * storage, and it is written where that function is.
29
+ *
30
+ * The hash is written out below rather than taken from a package, and that is a
31
+ * decision rather than an oversight. This package's runtime dependency tree is
32
+ * `zod` and nothing else, and it is the whole of what a merchant installs with
33
+ * the SDK — ADR-0003 §8 makes every addition to it a decision of its own, and
34
+ * that decision is not this file's to take. What is written out is therefore
35
+ * kept as small as the job allows: twenty-four rounds of a fixed permutation
36
+ * over one block, with no configuration and no second use, and it is pinned by
37
+ * the vectors the standard publishes, so an implementation that is wrong
38
+ * anywhere fails against somebody else's arithmetic rather than agreeing with
39
+ * itself. The cost is named too: this is hand-written cryptographic code in a
40
+ * published package, and the trigger to replace it with an audited library is
41
+ * the first other thing in this package that needs a hash.
42
+ */
43
+ import { z } from "zod";
44
+ /** How many characters an address carries after its prefix. */
45
+ const ADDRESS_LENGTH = 40;
46
+ /**
47
+ * What an address looks like before anything is checked about its letters.
48
+ *
49
+ * It is a pattern rather than a set of refinements for the reason written in
50
+ * `primitives.ts`: a rule expressed as a refinement is dropped from the JSON
51
+ * Schema export without a word, and this half of the rule is the half a
52
+ * generated client in another language can keep.
53
+ */
54
+ const ADDRESS_SHAPE = /^0x[0-9a-fA-F]{40}$/;
55
+ /**
56
+ * How wide a block of Keccak-256 is, in bytes: the state less twice the
57
+ * capacity the 256-bit digest asks for.
58
+ *
59
+ * Forty characters fit inside one, which is why nothing here loops over blocks.
60
+ */
61
+ const BLOCK_BYTES = 136;
62
+ /** The constant mixed into the state at the end of each of the twenty-four rounds. */
63
+ const ROUND_CONSTANTS = [
64
+ 0x0000000000000001n,
65
+ 0x0000000000008082n,
66
+ 0x800000000000808an,
67
+ 0x8000000080008000n,
68
+ 0x000000000000808bn,
69
+ 0x0000000080000001n,
70
+ 0x8000000080008081n,
71
+ 0x8000000000008009n,
72
+ 0x000000000000008an,
73
+ 0x0000000000000088n,
74
+ 0x0000000080008009n,
75
+ 0x000000008000000an,
76
+ 0x000000008000808bn,
77
+ 0x800000000000008bn,
78
+ 0x8000000000008089n,
79
+ 0x8000000000008003n,
80
+ 0x8000000000008002n,
81
+ 0x8000000000000080n,
82
+ 0x000000000000800an,
83
+ 0x800000008000000an,
84
+ 0x8000000080008081n,
85
+ 0x8000000000008080n,
86
+ 0x0000000080000001n,
87
+ 0x8000000080008008n,
88
+ ];
89
+ /** How far each lane is rotated as it is carried to its new place. */
90
+ const ROTATIONS = [
91
+ 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44,
92
+ ];
93
+ /** Where each lane goes, following the same path the rotations are read along. */
94
+ const DESTINATIONS = [
95
+ 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1,
96
+ ];
97
+ /** Sixty-four bits, since the arithmetic below is done in unbounded integers. */
98
+ const LANE = (1n << 64n) - 1n;
99
+ /**
100
+ * One lane of the state, by its index.
101
+ *
102
+ * Every index this file computes is inside the twenty-five lanes by
103
+ * construction — they come from the fixed tables above and from loops bounded
104
+ * by five. The fallback is what the compiler's index check asks for and is
105
+ * never reached.
106
+ */
107
+ const laneAt = (state, index) => state[index] ?? 0n;
108
+ /** One lane rotated left, the way the permutation rotates. */
109
+ const rotated = (lane, by) => ((lane << BigInt(by)) | (lane >> BigInt(64 - by))) & LANE;
110
+ /**
111
+ * The Keccak permutation, applied to the state in place.
112
+ *
113
+ * The four steps are the standard's own and are named after it: the column
114
+ * parities, the rotate-and-move, the row mixing, and the round constant.
115
+ */
116
+ function permute(state) {
117
+ for (const constant of ROUND_CONSTANTS) {
118
+ const parity = [];
119
+ for (let column = 0; column < 5; column++) {
120
+ parity[column] =
121
+ laneAt(state, column) ^
122
+ laneAt(state, column + 5) ^
123
+ laneAt(state, column + 10) ^
124
+ laneAt(state, column + 15) ^
125
+ laneAt(state, column + 20);
126
+ }
127
+ for (let column = 0; column < 5; column++) {
128
+ const change = laneAt(parity, (column + 4) % 5) ^ rotated(laneAt(parity, (column + 1) % 5), 1);
129
+ for (let row = 0; row < 25; row += 5) {
130
+ state[column + row] = laneAt(state, column + row) ^ change;
131
+ }
132
+ }
133
+ let carried = laneAt(state, 1);
134
+ for (let step = 0; step < 24; step++) {
135
+ const destination = DESTINATIONS[step] ?? 0;
136
+ const held = laneAt(state, destination);
137
+ state[destination] = rotated(carried, ROTATIONS[step] ?? 0);
138
+ carried = held;
139
+ }
140
+ for (let row = 0; row < 25; row += 5) {
141
+ const held = state.slice(row, row + 5);
142
+ for (let column = 0; column < 5; column++) {
143
+ state[row + column] =
144
+ laneAt(held, column) ^
145
+ (~laneAt(held, (column + 1) % 5) & LANE & laneAt(held, (column + 2) % 5));
146
+ }
147
+ }
148
+ state[0] = laneAt(state, 0) ^ constant;
149
+ }
150
+ }
151
+ /**
152
+ * The first forty hexadecimal digits of the Keccak-256 digest of an address
153
+ * written in lower case — one digit for each character of the address, which is
154
+ * exactly what the checksum reads.
155
+ *
156
+ * The input is always forty ASCII characters, so it is absorbed as one padded
157
+ * block and squeezed once. A general implementation would carry a loop over
158
+ * blocks and a branch neither of which anything here would ever take.
159
+ */
160
+ function checksumDigits(body) {
161
+ const block = new Uint8Array(BLOCK_BYTES);
162
+ for (let index = 0; index < ADDRESS_LENGTH; index++) {
163
+ block[index] = body.charCodeAt(index);
164
+ }
165
+ // The padding the original Keccak appends: a marker where the message ends,
166
+ // and the top bit of the last byte of the block.
167
+ block[ADDRESS_LENGTH] = 0x01;
168
+ block[BLOCK_BYTES - 1] = 0x80;
169
+ const state = new Array(25).fill(0n);
170
+ for (let lane = 0; lane < BLOCK_BYTES / 8; lane++) {
171
+ let value = 0n;
172
+ for (let byte = 7; byte >= 0; byte--) {
173
+ value = (value << 8n) | BigInt(block[lane * 8 + byte] ?? 0);
174
+ }
175
+ state[lane] = value;
176
+ }
177
+ permute(state);
178
+ let digits = "";
179
+ for (let lane = 0; digits.length < ADDRESS_LENGTH; lane++) {
180
+ let value = laneAt(state, lane);
181
+ for (let byte = 0; byte < 8; byte++) {
182
+ digits += (value & 0xffn).toString(16).padStart(2, "0");
183
+ value >>= 8n;
184
+ }
185
+ }
186
+ return digits.slice(0, ADDRESS_LENGTH);
187
+ }
188
+ /**
189
+ * One address body written the way a wallet writes it: every character whose
190
+ * digest digit is eight or more capitalised, and the rest left alone.
191
+ */
192
+ function checksummedBody(body) {
193
+ const digits = checksumDigits(body);
194
+ let written = "";
195
+ for (let index = 0; index < ADDRESS_LENGTH; index++) {
196
+ const character = body[index] ?? "";
197
+ // A digit has no capital form, so nothing has to ask whether this is a
198
+ // letter: capitalising "7" gives "7" back.
199
+ written += Number.parseInt(digits[index] ?? "0", 16) >= 8 ? character.toUpperCase() : character;
200
+ }
201
+ return written;
202
+ }
203
+ /**
204
+ * Whether the letters of this address say what the address says.
205
+ *
206
+ * A value whose shape is wrong is left alone rather than answered here: the
207
+ * pattern above has already complained about it, and two complaints about one
208
+ * string would send a merchant looking for a checksum in something that is not
209
+ * an address at all.
210
+ */
211
+ function carriesItsOwnChecksum(value) {
212
+ if (!ADDRESS_SHAPE.test(value)) {
213
+ return true;
214
+ }
215
+ const body = value.slice(2);
216
+ const lowered = body.toLowerCase();
217
+ return body === lowered || body === checksummedBody(lowered);
218
+ }
219
+ /**
220
+ * One address written the way the wallet it came out of displays it: the
221
+ * canonical form, and the one anything behind the door keeps.
222
+ *
223
+ * The door takes two spellings and everything behind it holds one (ADR-0017),
224
+ * and this is the one, because it is the one a person recognises. A merchant
225
+ * pastes forty characters out of their wallet and then reads them back on a
226
+ * settings screen; handed the same address in lower case they cannot tell it
227
+ * from a different address without comparing character by character, and the
228
+ * one field in this system where that matters is the field money is sent to.
229
+ * Lower case would be the easier canon to compute and the harder one to check
230
+ * by eye, and the eye is the only thing checking.
231
+ *
232
+ * It refuses what the schema refuses rather than quietly repairing it. Given a
233
+ * mixed-case address whose letters disagree with its digits, capitalising it
234
+ * afresh would produce a perfectly well-formed address that nobody typed —
235
+ * which is the exact failure the checksum exists to catch, arrived at by the
236
+ * thing meant to enforce it.
237
+ *
238
+ * Given an address that is already in this form it answers with the same
239
+ * string, so writing one down twice writes the same thing twice.
240
+ */
241
+ export function checksummedAddressOf(address) {
242
+ if (!ADDRESS_SHAPE.test(address) || !carriesItsOwnChecksum(address)) {
243
+ throw new Error(`${JSON.stringify(address)} is not an address this can write out: it is either not an address at all, or its capital letters disagree with the rest of it`);
244
+ }
245
+ return `0x${checksummedBody(address.slice(2).toLowerCase())}`;
246
+ }
247
+ /**
248
+ * An address on an EVM chain, in one of the two spellings anybody writes one
249
+ * in.
250
+ *
251
+ * It carries no chain of its own. The same twenty bytes are an address on every
252
+ * EVM network, and which network a payment is made on is settled by the payment
253
+ * requirements rather than by the address inside them; a schema that claimed
254
+ * otherwise would be claiming knowledge this package does not have.
255
+ */
256
+ export const EvmAddressSchema = z
257
+ .string()
258
+ .regex(ADDRESS_SHAPE, "an address on an EVM chain is 0x and then forty hexadecimal characters, with nothing else on it")
259
+ .refine(carriesItsOwnChecksum, "the capital letters in an address are a checksum over the address itself, and these do not" +
260
+ " match the rest of it — so a character in it is wrong, or something has changed its case" +
261
+ " along the way; paste it again exactly as your wallet shows it, or write it all in lower" +
262
+ " case")
263
+ .meta({
264
+ description: "An address on an EVM chain: 0x and forty hexadecimal characters. Two spellings are accepted and no others — all in lower case, which carries no checksum, and the exact mixed-case spelling a wallet shows, which is the EIP-55 checksum of the address. Anything between the two is refused, because the capitals are a checksum and letters that disagree with the digits mean a character is wrong; an address that is wrong is another perfectly valid address belonging to somebody else. That checksum is not part of this document: JSON Schema cannot express it, so a client generated from here checks the shape and this gateway checks the rest. What is stored and what comes back is always the mixed-case spelling a wallet shows, whichever of the two was sent — it is the form a person can recognise at a glance, which on the one field money is sent to is the whole of the checking anybody does.",
265
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * What a merchant's handler answers when an order reaches it.
3
+ *
4
+ * There are three answers and no more, and the closed set is the point. A
5
+ * refusal is understood as a final "this cannot be delivered" and closes the
6
+ * order; a temporary failure is expressed by an exception, a dead process or a
7
+ * dropped connection, all of which read as an order that was not delivered and
8
+ * comes back after a delay. A fourth answer meaning "not right now" would blur
9
+ * that line, and a supplier who timed out once would look like a product that
10
+ * cannot be sold at all.
11
+ *
12
+ * Silence is not an answer either. Every wait has a deadline, and an order
13
+ * that runs out of time is closed without the handler.
14
+ */
15
+ import { z } from "zod";
16
+ /**
17
+ * The three codes we and the merchant read the same way.
18
+ *
19
+ * Preferring them costs nothing and buys one thing in particular:
20
+ * `out_of_stock` is what feeds the share of purchases that ran into a missing
21
+ * product, which is a number we hold under a limit. The same refusal sent
22
+ * under a merchant's own code is invisible to it, and the picture of their
23
+ * catalog comes out wrong.
24
+ */
25
+ export declare const RECOMMENDED_REFUSAL_CODES: Readonly<{
26
+ readonly OUT_OF_STOCK: "out_of_stock";
27
+ readonly INVALID_PARAMS: "invalid_params";
28
+ readonly CANNOT_FULFILL: "cannot_fulfill";
29
+ }>;
30
+ /**
31
+ * A refusal code.
32
+ *
33
+ * The set is open: a merchant whose reason fits none of ours says it in their
34
+ * own word rather than flattening it into the nearest approximation. What the
35
+ * schema does insist on is that there is a code at all — a refusal nobody can
36
+ * count is a refusal that never shows up in any picture of what is going
37
+ * wrong.
38
+ */
39
+ export declare const RefusalCodeSchema: z.ZodString;
40
+ export declare const RefusalSchema: z.ZodObject<{
41
+ code: z.ZodString;
42
+ message: z.ZodString;
43
+ }, z.core.$strict>;
44
+ /**
45
+ * The delivery itself: the JSON the card's `result` declared.
46
+ *
47
+ * Here it is only held to being an object with names a card could have
48
+ * declared. Whether it matches this particular card is the job of the checker
49
+ * compiled from that card's `result` — one problem, one error message.
50
+ *
51
+ * The portal promises that a delivery reaches the agent as the merchant wrote
52
+ * it. There is one exception, and it is the one written down at
53
+ * `PROTOTYPE_KEY_IS_DROPPED` in `param-spec.ts` — of the five places that
54
+ * share it, this is the one where the loss lands in what the agent is handed
55
+ * rather than in what they sent.
56
+ *
57
+ * It is named and exported rather than left inside the answer below because
58
+ * two surfaces carry it: what a synchronous handler returns, and the body of
59
+ * the delivery call an asynchronous merchant makes (`api.ts`). Written out
60
+ * twice, the two would be one shape only until somebody edited one of them.
61
+ */
62
+ export declare const DeliverySchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
63
+ /**
64
+ * Taking an order on: the merchant will deliver, and here is how long they
65
+ * expect that to take, when they know. An empty acceptance is a complete
66
+ * answer.
67
+ *
68
+ * Named and exported for the same reason as the delivery above — it is both a
69
+ * handler's answer and the body of a call.
70
+ */
71
+ export declare const AcceptanceSchema: z.ZodObject<{
72
+ eta_seconds: z.ZodOptional<z.ZodInt>;
73
+ }, z.core.$strict>;
74
+ export declare const HandlerAnswerSchema: z.ZodUnion<readonly [z.ZodObject<{
75
+ delivered: z.ZodRecord<z.ZodString, z.ZodUnknown>;
76
+ }, z.core.$strict>, z.ZodObject<{
77
+ refused: z.ZodObject<{
78
+ code: z.ZodString;
79
+ message: z.ZodString;
80
+ }, z.core.$strict>;
81
+ }, z.core.$strict>, z.ZodObject<{
82
+ accepted: z.ZodObject<{
83
+ eta_seconds: z.ZodOptional<z.ZodInt>;
84
+ }, z.core.$strict>;
85
+ }, z.core.$strict>]>;
86
+ export type RefusalCode = z.infer<typeof RefusalCodeSchema>;
87
+ export type Refusal = z.infer<typeof RefusalSchema>;
88
+ export type Delivery = z.infer<typeof DeliverySchema>;
89
+ export type Acceptance = z.infer<typeof AcceptanceSchema>;
90
+ export type HandlerAnswer = z.infer<typeof HandlerAnswerSchema>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * What a merchant's handler answers when an order reaches it.
3
+ *
4
+ * There are three answers and no more, and the closed set is the point. A
5
+ * refusal is understood as a final "this cannot be delivered" and closes the
6
+ * order; a temporary failure is expressed by an exception, a dead process or a
7
+ * dropped connection, all of which read as an order that was not delivered and
8
+ * comes back after a delay. A fourth answer meaning "not right now" would blur
9
+ * that line, and a supplier who timed out once would look like a product that
10
+ * cannot be sold at all.
11
+ *
12
+ * Silence is not an answer either. Every wait has a deadline, and an order
13
+ * that runs out of time is closed without the handler.
14
+ */
15
+ import { z } from "zod";
16
+ import { ParamNameSchema } from "./param-spec.js";
17
+ /**
18
+ * The three codes we and the merchant read the same way.
19
+ *
20
+ * Preferring them costs nothing and buys one thing in particular:
21
+ * `out_of_stock` is what feeds the share of purchases that ran into a missing
22
+ * product, which is a number we hold under a limit. The same refusal sent
23
+ * under a merchant's own code is invisible to it, and the picture of their
24
+ * catalog comes out wrong.
25
+ */
26
+ export const RECOMMENDED_REFUSAL_CODES = Object.freeze({
27
+ OUT_OF_STOCK: "out_of_stock",
28
+ INVALID_PARAMS: "invalid_params",
29
+ CANNOT_FULFILL: "cannot_fulfill",
30
+ });
31
+ /**
32
+ * A refusal code.
33
+ *
34
+ * The set is open: a merchant whose reason fits none of ours says it in their
35
+ * own word rather than flattening it into the nearest approximation. What the
36
+ * schema does insist on is that there is a code at all — a refusal nobody can
37
+ * count is a refusal that never shows up in any picture of what is going
38
+ * wrong.
39
+ */
40
+ export const RefusalCodeSchema = z
41
+ .string()
42
+ .regex(/\S/, "a refusal carries a short code, even one of the merchant's own")
43
+ .meta({
44
+ // The dictionary has to travel with the field. An engineer generating a
45
+ // client outside TypeScript is the only reader this export exists for, and
46
+ // a bare `{type: "string"}` tells them nothing about the three codes we
47
+ // promise to read the same way — least of all that `out_of_stock` is what
48
+ // feeds the availability measure their catalog is held to.
49
+ description: 'A short code for why a delivery cannot happen. The set is open — a merchant whose reason fits none of ours sends their own word rather than the nearest approximation. Three are understood the same way on both sides and should be preferred: "out_of_stock" (the product is gone: sold out, no seats, the supplier did not deliver), "invalid_params" (the purchase parameters do not work for this delivery), "cannot_fulfill" (it cannot be delivered for some other reason). Only "out_of_stock" feeds the share of purchases that ran into a missing product, which is a number we hold under a limit — the same refusal sent under a merchant\'s own code is invisible to it.',
50
+ });
51
+ export const RefusalSchema = z.strictObject({
52
+ code: RefusalCodeSchema,
53
+ /**
54
+ * The reason, in words. We and the agent read the code; this is for the
55
+ * person who picks the case up afterwards, and leaving it empty leaves them
56
+ * with nothing.
57
+ */
58
+ message: z.string().regex(/\S/, "a refusal carries a reason a person can read"),
59
+ });
60
+ /**
61
+ * The delivery itself: the JSON the card's `result` declared.
62
+ *
63
+ * Here it is only held to being an object with names a card could have
64
+ * declared. Whether it matches this particular card is the job of the checker
65
+ * compiled from that card's `result` — one problem, one error message.
66
+ *
67
+ * The portal promises that a delivery reaches the agent as the merchant wrote
68
+ * it. There is one exception, and it is the one written down at
69
+ * `PROTOTYPE_KEY_IS_DROPPED` in `param-spec.ts` — of the five places that
70
+ * share it, this is the one where the loss lands in what the agent is handed
71
+ * rather than in what they sent.
72
+ *
73
+ * It is named and exported rather than left inside the answer below because
74
+ * two surfaces carry it: what a synchronous handler returns, and the body of
75
+ * the delivery call an asynchronous merchant makes (`api.ts`). Written out
76
+ * twice, the two would be one shape only until somebody edited one of them.
77
+ */
78
+ export const DeliverySchema = z.record(ParamNameSchema, z.unknown()).meta({
79
+ description: "What the merchant delivers: the fields the card's result declared. This document holds the names to the shape a card could have declared and no further. That they are the names this particular card declared is checked against that card, and no document standing on its own can express it.",
80
+ });
81
+ /**
82
+ * Taking an order on: the merchant will deliver, and here is how long they
83
+ * expect that to take, when they know. An empty acceptance is a complete
84
+ * answer.
85
+ *
86
+ * Named and exported for the same reason as the delivery above — it is both a
87
+ * handler's answer and the body of a call.
88
+ */
89
+ export const AcceptanceSchema = z
90
+ .strictObject({
91
+ eta_seconds: z.int().positive().optional(),
92
+ })
93
+ .meta({
94
+ description: "Taking an order on: the merchant will deliver it. eta_seconds is how long they expect that to take, when they know; leaving it out is a complete answer and not a refusal to say. It is an expectation and not a commitment — what the merchant is actually held to is the delivery deadline on the card, and the two are different numbers.",
95
+ });
96
+ export const HandlerAnswerSchema = z.union([
97
+ z.strictObject({ delivered: DeliverySchema }),
98
+ z.strictObject({ refused: RefusalSchema }),
99
+ z.strictObject({ accepted: AcceptanceSchema }),
100
+ ], {
101
+ error: "a handler answers with exactly one of { delivered }, { refused } or { accepted }; a temporary failure is an exception, not a refusal",
102
+ });