@defdone/annon 0.0.0-stage → 0.1.0-alpha.1

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 (214) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/CLI.md +378 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +7 -0
  5. package/README.md +591 -2
  6. package/dist/api/annon-types.d.ts +108 -0
  7. package/dist/api/annon-types.js +2 -0
  8. package/dist/api/annon.d.ts +117 -0
  9. package/dist/api/annon.js +1152 -0
  10. package/dist/api/hosted-record.d.ts +13 -0
  11. package/dist/api/hosted-record.js +149 -0
  12. package/dist/api/sharing-types.d.ts +109 -0
  13. package/dist/api/sharing-types.js +1 -0
  14. package/dist/api/sharing.d.ts +36 -0
  15. package/dist/api/sharing.js +879 -0
  16. package/dist/browser/download.d.ts +19 -0
  17. package/dist/browser/download.js +52 -0
  18. package/dist/browser/index.d.ts +23 -0
  19. package/dist/browser/index.js +22 -0
  20. package/dist/browser/login-bundle.js +151 -0
  21. package/dist/browser/login.d.ts +1 -0
  22. package/dist/browser/login.js +216 -0
  23. package/dist/browser/purchase-bundle.js +84 -0
  24. package/dist/browser/purchases.d.ts +98 -0
  25. package/dist/browser/purchases.js +735 -0
  26. package/dist/browser/record.d.ts +55 -0
  27. package/dist/browser/record.js +16 -0
  28. package/dist/browser/selling.d.ts +146 -0
  29. package/dist/browser/selling.js +418 -0
  30. package/dist/browser/wallets.d.ts +53 -0
  31. package/dist/browser/wallets.js +286 -0
  32. package/dist/bytes.d.ts +4 -0
  33. package/dist/bytes.js +16 -0
  34. package/dist/download/download.d.ts +59 -0
  35. package/dist/download/download.js +384 -0
  36. package/dist/download/host.d.ts +56 -0
  37. package/dist/download/host.js +161 -0
  38. package/dist/download/http.d.ts +23 -0
  39. package/dist/download/http.js +87 -0
  40. package/dist/download/proof.d.ts +20 -0
  41. package/dist/download/proof.js +79 -0
  42. package/dist/download/upload.d.ts +130 -0
  43. package/dist/download/upload.js +408 -0
  44. package/dist/envelope/evm.d.ts +4 -0
  45. package/dist/envelope/evm.js +32 -0
  46. package/dist/envelope/pay2.d.ts +176 -0
  47. package/dist/envelope/pay2.js +541 -0
  48. package/dist/envelope/replay.d.ts +45 -0
  49. package/dist/envelope/replay.js +186 -0
  50. package/dist/envelope/stream.d.ts +78 -0
  51. package/dist/envelope/stream.js +372 -0
  52. package/dist/errors.d.ts +21 -0
  53. package/dist/errors.js +300 -0
  54. package/dist/index.d.ts +25 -0
  55. package/dist/index.js +23 -0
  56. package/dist/keys/near-ckd.d.ts +47 -0
  57. package/dist/keys/near-ckd.js +96 -0
  58. package/dist/limits.d.ts +4 -0
  59. package/dist/limits.js +4 -0
  60. package/dist/near/nep413.d.ts +22 -0
  61. package/dist/near/nep413.js +39 -0
  62. package/dist/near/rpc.d.ts +170 -0
  63. package/dist/near/rpc.js +423 -0
  64. package/dist/near/texts.d.ts +39 -0
  65. package/dist/near/texts.js +60 -0
  66. package/dist/net/ip.d.ts +7 -0
  67. package/dist/net/ip.js +62 -0
  68. package/dist/net/retry.d.ts +3 -0
  69. package/dist/net/retry.js +31 -0
  70. package/dist/node/adapter-checks.d.ts +6 -0
  71. package/dist/node/adapter-checks.js +13 -0
  72. package/dist/node/adapter.d.ts +8 -0
  73. package/dist/node/adapter.js +98 -0
  74. package/dist/node/agent.d.ts +49 -0
  75. package/dist/node/agent.js +266 -0
  76. package/dist/node/amounts.d.ts +2 -0
  77. package/dist/node/amounts.js +20 -0
  78. package/dist/node/browser-login.d.ts +25 -0
  79. package/dist/node/browser-login.js +219 -0
  80. package/dist/node/budget.d.ts +24 -0
  81. package/dist/node/budget.js +59 -0
  82. package/dist/node/buy-commands.d.ts +24 -0
  83. package/dist/node/buy-commands.js +301 -0
  84. package/dist/node/cli.d.ts +60 -0
  85. package/dist/node/cli.js +201 -0
  86. package/dist/node/command-context.d.ts +82 -0
  87. package/dist/node/command-context.js +77 -0
  88. package/dist/node/config.d.ts +37 -0
  89. package/dist/node/config.js +189 -0
  90. package/dist/node/core-adapter.d.ts +43 -0
  91. package/dist/node/core-adapter.js +122 -0
  92. package/dist/node/errors.d.ts +8 -0
  93. package/dist/node/errors.js +12 -0
  94. package/dist/node/index.d.ts +7 -0
  95. package/dist/node/index.js +5 -0
  96. package/dist/node/io.d.ts +13 -0
  97. package/dist/node/io.js +112 -0
  98. package/dist/node/keychain.d.ts +10 -0
  99. package/dist/node/keychain.js +67 -0
  100. package/dist/node/main.d.ts +2 -0
  101. package/dist/node/main.js +21 -0
  102. package/dist/node/offers.d.ts +16 -0
  103. package/dist/node/offers.js +91 -0
  104. package/dist/node/origin.d.ts +11 -0
  105. package/dist/node/origin.js +80 -0
  106. package/dist/node/paths.d.ts +3 -0
  107. package/dist/node/paths.js +71 -0
  108. package/dist/node/payment-terms.d.ts +26 -0
  109. package/dist/node/payment-terms.js +127 -0
  110. package/dist/node/purchase-commands.d.ts +16 -0
  111. package/dist/node/purchase-commands.js +237 -0
  112. package/dist/node/purchase-guard.d.ts +19 -0
  113. package/dist/node/purchase-guard.js +53 -0
  114. package/dist/node/relay-transport.d.ts +6 -0
  115. package/dist/node/relay-transport.js +173 -0
  116. package/dist/node/resources.d.ts +2 -0
  117. package/dist/node/resources.js +26 -0
  118. package/dist/node/retry-gate.d.ts +15 -0
  119. package/dist/node/retry-gate.js +53 -0
  120. package/dist/node/seller-commands.d.ts +12 -0
  121. package/dist/node/seller-commands.js +207 -0
  122. package/dist/node/sharing.d.ts +3 -0
  123. package/dist/node/sharing.js +28 -0
  124. package/dist/node/shops.d.ts +17 -0
  125. package/dist/node/shops.js +74 -0
  126. package/dist/node/signer-mock.d.ts +11 -0
  127. package/dist/node/signer-mock.js +79 -0
  128. package/dist/node/signer-wallet.d.ts +8 -0
  129. package/dist/node/signer-wallet.js +149 -0
  130. package/dist/node/signing.d.ts +29 -0
  131. package/dist/node/signing.js +200 -0
  132. package/dist/node/storage-commands.d.ts +11 -0
  133. package/dist/node/storage-commands.js +50 -0
  134. package/dist/node/sync-dir.d.ts +1 -0
  135. package/dist/node/sync-dir.js +11 -0
  136. package/dist/node/wallets.d.ts +13 -0
  137. package/dist/node/wallets.js +146 -0
  138. package/dist/oneclick/oneclick.d.ts +132 -0
  139. package/dist/oneclick/oneclick.js +342 -0
  140. package/dist/pay/backup.d.ts +70 -0
  141. package/dist/pay/backup.js +402 -0
  142. package/dist/pay/base.d.ts +135 -0
  143. package/dist/pay/base.js +386 -0
  144. package/dist/pay/eip3009.d.ts +59 -0
  145. package/dist/pay/eip3009.js +72 -0
  146. package/dist/pay/pay.d.ts +86 -0
  147. package/dist/pay/pay.js +297 -0
  148. package/dist/pay/purchase.d.ts +103 -0
  149. package/dist/pay/purchase.js +20 -0
  150. package/dist/pay/recover.d.ts +31 -0
  151. package/dist/pay/recover.js +34 -0
  152. package/dist/pay/seller.d.ts +130 -0
  153. package/dist/pay/seller.js +444 -0
  154. package/dist/pay/settle.d.ts +96 -0
  155. package/dist/pay/settle.js +308 -0
  156. package/dist/pay/split.d.ts +115 -0
  157. package/dist/pay/split.js +256 -0
  158. package/dist/pay/store-node.d.ts +8 -0
  159. package/dist/pay/store-node.js +224 -0
  160. package/dist/pay/store.d.ts +54 -0
  161. package/dist/pay/store.js +135 -0
  162. package/dist/pay/x402.d.ts +95 -0
  163. package/dist/pay/x402.js +136 -0
  164. package/dist/relay/admission.d.ts +8 -0
  165. package/dist/relay/admission.js +58 -0
  166. package/dist/relay/capabilities.d.ts +36 -0
  167. package/dist/relay/capabilities.js +232 -0
  168. package/dist/relay/fees.d.ts +17 -0
  169. package/dist/relay/fees.js +105 -0
  170. package/dist/relay/index.d.ts +10 -0
  171. package/dist/relay/index.js +8 -0
  172. package/dist/relay/keeper-balance.d.ts +17 -0
  173. package/dist/relay/keeper-balance.js +46 -0
  174. package/dist/relay/keeper-runner.d.ts +114 -0
  175. package/dist/relay/keeper-runner.js +703 -0
  176. package/dist/relay/keeper.d.ts +38 -0
  177. package/dist/relay/keeper.js +73 -0
  178. package/dist/relay/ledger.d.ts +155 -0
  179. package/dist/relay/ledger.js +392 -0
  180. package/dist/relay/relay-checks.d.ts +3 -0
  181. package/dist/relay/relay-checks.js +244 -0
  182. package/dist/relay/relay-reconciliation.d.ts +15 -0
  183. package/dist/relay/relay-reconciliation.js +293 -0
  184. package/dist/relay/relay-request.d.ts +6 -0
  185. package/dist/relay/relay-request.js +139 -0
  186. package/dist/relay/relay-transactions.d.ts +23 -0
  187. package/dist/relay/relay-transactions.js +182 -0
  188. package/dist/relay/relay-types.d.ts +128 -0
  189. package/dist/relay/relay-types.js +17 -0
  190. package/dist/relay/relay.d.ts +102 -0
  191. package/dist/relay/relay.js +426 -0
  192. package/dist/relay/reservation.d.ts +29 -0
  193. package/dist/relay/reservation.js +132 -0
  194. package/dist/relay/sponsorship.d.ts +42 -0
  195. package/dist/relay/sponsorship.js +184 -0
  196. package/dist/routes.d.ts +2 -0
  197. package/dist/routes.js +1 -0
  198. package/dist/settle-near/codec.d.ts +17 -0
  199. package/dist/settle-near/codec.js +86 -0
  200. package/dist/settle-near/identity.d.ts +27 -0
  201. package/dist/settle-near/identity.js +80 -0
  202. package/dist/settle-near/purchase.d.ts +40 -0
  203. package/dist/settle-near/purchase.js +153 -0
  204. package/dist/settle-near/terms.d.ts +37 -0
  205. package/dist/settle-near/terms.js +89 -0
  206. package/dist/shops/discovery.d.ts +95 -0
  207. package/dist/shops/discovery.js +384 -0
  208. package/dist/wallet/account-only.d.ts +11 -0
  209. package/dist/wallet/account-only.js +6 -0
  210. package/dist/wallet/uncertain.d.ts +8 -0
  211. package/dist/wallet/uncertain.js +13 -0
  212. package/dist/wallet/wallet.d.ts +100 -0
  213. package/dist/wallet/wallet.js +204 -0
  214. package/package.json +136 -3
@@ -0,0 +1,146 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { ed25519 } from '@noble/curves/ed25519.js';
3
+ import { base64 } from '@scure/base';
4
+ import { Account, KeyPair } from 'near-api-js';
5
+ import { createPublicClient, createWalletClient, http } from 'viem';
6
+ import { privateKeyToAccount } from 'viem/accounts';
7
+ import { base } from 'viem/chains';
8
+ import { signNearUpload } from '../browser/wallets.js';
9
+ import { nep413Hash } from '../near/nep413.js';
10
+ import { bytes, hex } from '../settle-near/codec.js';
11
+ import { identityJson, parseIdentity } from '../settle-near/identity.js';
12
+ import { nearKeyPairWallet } from '../wallet/wallet.js';
13
+ import { AdapterError } from './errors.js';
14
+ import { decodeKeychain, osKeychain } from './keychain.js';
15
+ const credentialsError = () => new AdapterError('credentials_required', 'Select a wallet and provide its key through the documented environment variable or a named key file. Check the recovery identity matches.', 2);
16
+ export function identity(value) {
17
+ const at = value.indexOf(':');
18
+ return parseIdentity({ kind: value.slice(0, at), id: value.slice(at + 1) });
19
+ }
20
+ async function source(file, env) {
21
+ if (file)
22
+ return (await readFile(file, 'utf8')).trim();
23
+ if (env)
24
+ return env.trim();
25
+ throw credentialsError();
26
+ }
27
+ export async function readBackupSecret(o, env) {
28
+ try {
29
+ const text = await source(o.backupSecretFile ?? env.ANNON_BACKUP_SECRET_FILE, env.ANNON_BACKUP_SECRET);
30
+ return bytes(text.startsWith('0x') ? text : `0x${text}`, 32);
31
+ }
32
+ catch {
33
+ throw new AdapterError('backup_secret_required', 'Provide a 32-byte random backup secret as 64 hex characters in ANNON_BACKUP_SECRET or a named backup secret file.', 2);
34
+ }
35
+ }
36
+ export async function loadWallet(o, env, nearRpc, keychain = osKeychain) {
37
+ try {
38
+ const selected = o.wallet ?? (o.recovery ? identity(o.recovery).kind : env.ANNON_WALLET);
39
+ let wallet;
40
+ if (selected === 'evm') {
41
+ const key = await source(o.evmKeyFile ?? env.ANNON_EVM_KEY_FILE, env.ANNON_EVM_PRIVATE_KEY);
42
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key))
43
+ throw credentialsError();
44
+ const account = privateKeyToAccount(key);
45
+ const evm = {
46
+ identity: parseIdentity({ kind: 'evm', id: account.address.toLowerCase() }),
47
+ address: account.address.toLowerCase(),
48
+ authorize: async (text) => ({
49
+ kind: 'evm',
50
+ sig: await account.signMessage({ message: text }),
51
+ }),
52
+ signTypedData: (data) => account.signTypedData(data),
53
+ async sendBase(call, rpc) {
54
+ const client = createWalletClient({
55
+ account,
56
+ chain: base,
57
+ transport: http(rpc, { fetchOptions: { credentials: 'omit', redirect: 'error' } }),
58
+ });
59
+ const publicClient = createPublicClient({
60
+ chain: base,
61
+ transport: http(rpc, { fetchOptions: { credentials: 'omit', redirect: 'error' } }),
62
+ });
63
+ if ((await publicClient.getChainId()) !== base.id)
64
+ throw new Error('wrong Base chain');
65
+ const hash = await client.sendTransaction({
66
+ to: call.to,
67
+ data: call.data,
68
+ });
69
+ if ((await publicClient.waitForTransactionReceipt({ hash })).status !== 'success')
70
+ throw new Error('split creation failed');
71
+ },
72
+ };
73
+ wallet = evm;
74
+ }
75
+ else if (selected === 'solana') {
76
+ const value = JSON.parse(await source(o.solanaKeyFile ?? env.ANNON_SOLANA_KEY_FILE, env.ANNON_SOLANA_KEYPAIR));
77
+ if (!Array.isArray(value) ||
78
+ value.length !== 64 ||
79
+ value.some((v) => !Number.isInteger(v) || v < 0 || v > 255))
80
+ throw credentialsError();
81
+ const key = Uint8Array.from(value);
82
+ const seed = key.subarray(0, 32);
83
+ const publicKey = ed25519.getPublicKey(seed);
84
+ if (hex(publicKey) !== hex(key.subarray(32)))
85
+ throw credentialsError();
86
+ wallet = {
87
+ identity: parseIdentity({ kind: 'solana', id: hex(publicKey).slice(2) }),
88
+ authorize: async (text) => ({
89
+ kind: 'solana',
90
+ sig: hex(ed25519.sign(new TextEncoder().encode(text), seed)),
91
+ }),
92
+ };
93
+ }
94
+ else if (selected === 'near') {
95
+ const file = o.nearKeyFile ?? env.ANNON_NEAR_KEY_FILE;
96
+ let raw;
97
+ if (file || env.ANNON_NEAR_CREDENTIALS)
98
+ raw = await source(file, env.ANNON_NEAR_CREDENTIALS);
99
+ else {
100
+ const account = o.nearAccount ?? env.ANNON_NEAR_ACCOUNT;
101
+ const publicKey = o.nearPublicKey ?? env.ANNON_NEAR_PUBLIC_KEY;
102
+ const network = (o.network ?? env.ANNON_NETWORK ?? 'near:mainnet').replace(/^near:/, '');
103
+ if (!account ||
104
+ !publicKey ||
105
+ !/^ed25519:[1-9A-HJ-NP-Za-km-z]+$/.test(publicKey) ||
106
+ !['mainnet', 'testnet'].includes(network))
107
+ throw credentialsError();
108
+ parseIdentity({ kind: 'near', id: account });
109
+ raw = decodeKeychain(await keychain.read(`near-${network}-${account}/${account}:${publicKey}`));
110
+ }
111
+ const c = JSON.parse(raw);
112
+ const accountId = c.account_id ?? o.nearAccount ?? env.ANNON_NEAR_ACCOUNT;
113
+ if (!accountId || !c.private_key?.startsWith('ed25519:'))
114
+ throw credentialsError();
115
+ parseIdentity({ kind: 'near', id: accountId });
116
+ const key = KeyPair.fromString(c.private_key);
117
+ const expected = o.nearPublicKey ?? env.ANNON_NEAR_PUBLIC_KEY ?? c.public_key;
118
+ if (expected && key.getPublicKey().toString() !== expected)
119
+ throw credentialsError();
120
+ if ((o.nearAccount ?? env.ANNON_NEAR_ACCOUNT ?? accountId) !== accountId)
121
+ throw credentialsError();
122
+ // Transport exception near_api_local_wallet: Account owns nonce lookup and broadcast.
123
+ // Its provider uses the third-party Node fetch transport.
124
+ const account = new Account(accountId, nearRpc, key.toString());
125
+ wallet = {
126
+ ...nearKeyPairWallet(account),
127
+ signUpload: (intent) => signNearUpload({
128
+ getAccounts: async () => [{ accountId }],
129
+ signMessage: async (payload) => ({
130
+ accountId,
131
+ publicKey: key.getPublicKey().toString(),
132
+ signature: base64.encode(key.sign(nep413Hash(payload)).signature),
133
+ }),
134
+ }, intent, nearRpc),
135
+ };
136
+ }
137
+ else
138
+ throw credentialsError();
139
+ if (o.recovery && identityJson(identity(o.recovery)) !== identityJson(wallet.identity))
140
+ throw credentialsError();
141
+ return wallet;
142
+ }
143
+ catch {
144
+ throw credentialsError();
145
+ }
146
+ }
@@ -0,0 +1,132 @@
1
+ import { AnnonError } from '../errors.js';
2
+ export interface Origin {
3
+ asset: string;
4
+ chain: 'evm' | 'solana' | 'near' | 'other';
5
+ refundTo: string;
6
+ refundType?: 'ORIGIN_CHAIN' | 'INTENTS';
7
+ blockchain?: string;
8
+ depositMode?: 'SIMPLE' | 'MEMO';
9
+ }
10
+ export interface Token {
11
+ assetId: string;
12
+ blockchain: string;
13
+ symbol: string;
14
+ decimals: number;
15
+ [key: string]: unknown;
16
+ }
17
+ export declare function originFamily(blockchain: string): Origin['chain'];
18
+ /** Exact catalog selection. Unsupported and slow chains are deliberately unavailable. */
19
+ export declare function originFromToken(tokens: readonly Token[], asset: string, blockchain: string, refundTo: string): Origin;
20
+ export declare function validateRefundAddress(chain: Origin['chain'], value: string): void;
21
+ /** Catalog origins bind an exact blockchain; other families use explicit refund syntax. */
22
+ export declare function validateOrigin(origin: Origin): void;
23
+ export interface QuotePreview {
24
+ request: QuoteRequest;
25
+ amountIn: string;
26
+ amountOut: string;
27
+ timeEstimate: number;
28
+ }
29
+ export interface PollCheckpoint {
30
+ address: string;
31
+ memo?: string;
32
+ attempts: number;
33
+ errors: number;
34
+ lastStatus?: SwapStatus;
35
+ nextPollAt?: number;
36
+ }
37
+ export interface PollOptions {
38
+ memo?: string;
39
+ signal?: AbortSignal;
40
+ maxPolls?: number;
41
+ intervalMs?: number;
42
+ checkpoint?: PollCheckpoint;
43
+ /** Awaited so a durable checkpoint can be saved before polling again. */
44
+ onProgress?: (checkpoint: PollCheckpoint) => void | Promise<void>;
45
+ }
46
+ export interface QuoteRequest {
47
+ dry: boolean;
48
+ swapType: 'EXACT_OUTPUT';
49
+ slippageTolerance: number;
50
+ originAsset: string;
51
+ depositType: 'ORIGIN_CHAIN';
52
+ depositMode?: 'SIMPLE' | 'MEMO';
53
+ destinationAsset: string;
54
+ amount: string;
55
+ refundTo: string;
56
+ refundType: 'ORIGIN_CHAIN' | 'INTENTS';
57
+ recipient: string;
58
+ recipientType: 'DESTINATION_CHAIN';
59
+ deadline: string;
60
+ customRecipientMsg: string;
61
+ }
62
+ export interface Deposit {
63
+ address: string;
64
+ amount: string;
65
+ asset: string;
66
+ chain: Origin['chain'];
67
+ /** Exact catalog blockchain, when supplied by a catalog-selected origin. */
68
+ blockchain?: string;
69
+ memo?: string;
70
+ qrData: string;
71
+ /** Deposit must be confirmed by this earlier cutoff; delivery has a ten-minute buffer. */
72
+ depositBefore: string;
73
+ purchaseExpiresAt: string;
74
+ }
75
+ export interface OneClickQuote {
76
+ request: QuoteRequest;
77
+ response: Record<string, unknown>;
78
+ deposit: Deposit;
79
+ }
80
+ declare const states: readonly ["PENDING_DEPOSIT", "KNOWN_DEPOSIT_TX", "PROCESSING", "SUCCESS", "REFUNDED", "FAILED", "INCOMPLETE_DEPOSIT"];
81
+ export interface SwapStatus {
82
+ status: (typeof states)[number];
83
+ [key: string]: unknown;
84
+ }
85
+ export interface OneClickOptions {
86
+ endpoint?: string;
87
+ fetch?: typeof fetch;
88
+ now?: () => number;
89
+ wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
90
+ }
91
+ export declare class OneClickQuoteExpired extends AnnonError {
92
+ readonly code = "quote_expired";
93
+ constructor();
94
+ }
95
+ /** Only recognized public service messages may cross the CLI/MCP boundary. */
96
+ export declare class OneClickRefused extends AnnonError {
97
+ readonly code = "quote_refused";
98
+ readonly minimumAmountUsdc?: string;
99
+ constructor(message: string);
100
+ }
101
+ export declare class OneClick {
102
+ private readonly options;
103
+ constructor(options?: OneClickOptions);
104
+ private request;
105
+ private now;
106
+ tokens(signal?: AbortSignal): Promise<Token[]>;
107
+ preview(message: string, origin: Origin, options?: {
108
+ slippageTolerance?: number;
109
+ signal?: AbortSignal;
110
+ }): Promise<QuotePreview>;
111
+ quote(message: string, origin: Origin, options: {
112
+ dry: true;
113
+ slippageTolerance?: number;
114
+ signal?: AbortSignal;
115
+ }): Promise<QuotePreview>;
116
+ quote(message: string, origin: Origin, options?: {
117
+ dry?: false;
118
+ slippageTolerance?: number;
119
+ signal?: AbortSignal;
120
+ }): Promise<OneClickQuote>;
121
+ quote(message: string, origin: Origin, options: {
122
+ dry?: boolean;
123
+ slippageTolerance?: number;
124
+ signal?: AbortSignal;
125
+ }): Promise<OneClickQuote | QuotePreview>;
126
+ status(depositAddress: string, options?: {
127
+ memo?: string;
128
+ signal?: AbortSignal;
129
+ }): Promise<SwapStatus>;
130
+ poll(address: string, options?: PollOptions): Promise<SwapStatus>;
131
+ }
132
+ export {};
@@ -0,0 +1,342 @@
1
+ /** Native NEAR USDC delivery through 1Click. SUCCESS is delivery, not purchase acceptance. */
2
+ import { base58 } from '@scure/base';
3
+ import { isAddress } from 'viem/utils';
4
+ import { AnnonError, isTransient } from '../errors.js';
5
+ import { parseRetryAfterMs, sleep } from '../net/retry.js';
6
+ import { account, uint, usdc } from '../settle-near/codec.js';
7
+ import { parsePurchaseMessage } from '../settle-near/purchase.js';
8
+ import { NEAR_USDC } from '../settle-near/terms.js';
9
+ const evmChains = [
10
+ 'eth',
11
+ 'base',
12
+ 'arb',
13
+ 'arbitrum',
14
+ 'op',
15
+ 'optimism',
16
+ 'polygon',
17
+ 'pol',
18
+ 'bera',
19
+ 'scroll',
20
+ 'monad',
21
+ 'xlayer',
22
+ 'abs',
23
+ 'plasma',
24
+ 'bsc',
25
+ 'avax',
26
+ 'avalanche',
27
+ 'gnosis',
28
+ ];
29
+ export function originFamily(blockchain) {
30
+ if (evmChains.includes(blockchain))
31
+ return 'evm';
32
+ if (blockchain === 'sol' || blockchain === 'solana')
33
+ return 'solana';
34
+ if (blockchain === 'near')
35
+ return 'near';
36
+ return 'other';
37
+ }
38
+ function chainFamily(blockchain) {
39
+ const family = originFamily(blockchain);
40
+ if (family === 'other')
41
+ throw new TypeError('unsupported origin chain or purchase window');
42
+ return family;
43
+ }
44
+ /** Exact catalog selection. Unsupported and slow chains are deliberately unavailable. */
45
+ export function originFromToken(tokens, asset, blockchain, refundTo) {
46
+ if (tokens.filter((t) => t.assetId === asset && t.blockchain === blockchain).length !== 1)
47
+ throw new TypeError('select an exact catalog asset and blockchain');
48
+ const chain = chainFamily(blockchain);
49
+ validateRefundAddress(chain, refundTo);
50
+ return { asset, blockchain, chain, refundTo, refundType: 'ORIGIN_CHAIN' };
51
+ }
52
+ export function validateRefundAddress(chain, value) {
53
+ if (typeof value !== 'string' || !value || /\s/.test(value))
54
+ throw new TypeError('invalid refund address');
55
+ if (chain === 'evm') {
56
+ if (!isAddress(value) || /^0x0{40}$/i.test(value))
57
+ throw new TypeError('invalid EVM refund address');
58
+ }
59
+ else if (chain === 'solana') {
60
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value) || base58.decode(value).length !== 32)
61
+ throw new TypeError('invalid Solana refund address');
62
+ }
63
+ else if (chain === 'near')
64
+ account(value);
65
+ else if (chain !== 'other')
66
+ throw new TypeError('unsupported refund chain family');
67
+ }
68
+ /** Catalog origins bind an exact blockchain; other families use explicit refund syntax. */
69
+ export function validateOrigin(origin) {
70
+ if (!origin ||
71
+ typeof origin.asset !== 'string' ||
72
+ !origin.asset.startsWith('nep141:') ||
73
+ !origin.asset.slice(7) ||
74
+ /\s/.test(origin.asset))
75
+ throw new TypeError('invalid origin asset');
76
+ if (origin.blockchain && chainFamily(origin.blockchain) !== origin.chain)
77
+ throw new TypeError('origin chain family mismatch');
78
+ if (origin.depositMode !== undefined && !['SIMPLE', 'MEMO'].includes(origin.depositMode))
79
+ throw new TypeError('invalid deposit mode');
80
+ if (origin.refundType !== undefined && !['ORIGIN_CHAIN', 'INTENTS'].includes(origin.refundType))
81
+ throw new TypeError('invalid refund type');
82
+ validateRefundAddress(origin.chain, origin.refundTo);
83
+ if (origin.refundType === 'INTENTS')
84
+ account(origin.refundTo);
85
+ }
86
+ class TransportError extends AnnonError {
87
+ retryAfterMs;
88
+ constructor(retryAfterMs = 0) {
89
+ super('transport', '1Click transport unavailable');
90
+ this.retryAfterMs = retryAfterMs;
91
+ this.name = 'TransportError';
92
+ }
93
+ }
94
+ const states = [
95
+ 'PENDING_DEPOSIT',
96
+ 'KNOWN_DEPOSIT_TX',
97
+ 'PROCESSING',
98
+ 'SUCCESS',
99
+ 'REFUNDED',
100
+ 'FAILED',
101
+ 'INCOMPLETE_DEPOSIT',
102
+ ];
103
+ export class OneClickQuoteExpired extends AnnonError {
104
+ code = 'quote_expired';
105
+ constructor() {
106
+ super('quote_expired', 'The 1Click quote expired. Run buy again.');
107
+ this.name = 'OneClickQuoteExpired';
108
+ }
109
+ }
110
+ /** Only recognized public service messages may cross the CLI/MCP boundary. */
111
+ export class OneClickRefused extends AnnonError {
112
+ code = 'quote_refused';
113
+ minimumAmountUsdc;
114
+ constructor(message) {
115
+ super('quote_refused', message);
116
+ this.name = 'OneClickRefused';
117
+ const minimum = /^Amount is too low for bridge, try at least ([0-9]{1,39})[.]?$/i.exec(message);
118
+ if (minimum?.[1])
119
+ this.minimumAmountUsdc = usdc(minimum[1]);
120
+ }
121
+ }
122
+ function refusalMessage(value) {
123
+ // Do not expose arbitrary upstream text: it can echo requests, credentials or signed payloads.
124
+ return (typeof value === 'string' &&
125
+ (/^Amount is too low for bridge, try at least [0-9]{1,39}[.]?$/i.test(value) ||
126
+ /^(No routes found|No liquidity available|Amount is too low|Amount is too high|Unsupported (origin|destination) asset)[.]?$/i.test(value)));
127
+ }
128
+ /** A dry response has no deposit destination or inactivity deadline. */
129
+ function parseQuotePreview(request, response) {
130
+ const q = response.quote;
131
+ if (!q ||
132
+ q.amountOut !== request.amount ||
133
+ (q.minAmountOut !== undefined && uint(q.minAmountOut, 128) < BigInt(request.amount)))
134
+ throw new Error('1Click quote has wrong output');
135
+ const echo = response.quoteRequest;
136
+ if (!echo || Object.entries(request).some(([key, value]) => echo[key] !== value))
137
+ throw new Error('1Click quoteRequest echo mismatch');
138
+ if (typeof q.timeEstimate !== 'number' ||
139
+ !Number.isFinite(q.timeEstimate) ||
140
+ q.timeEstimate < 0 ||
141
+ q.timeEstimate > 600)
142
+ throw new Error('1Click delivery estimate exceeds purchase delivery window');
143
+ if (typeof q.amountIn !== 'string' || uint(q.amountIn, 128) === 0n)
144
+ throw new Error('invalid quote input amount');
145
+ return {
146
+ request,
147
+ amountIn: q.amountIn,
148
+ amountOut: request.amount,
149
+ timeEstimate: q.timeEstimate,
150
+ };
151
+ }
152
+ export class OneClick {
153
+ options;
154
+ constructor(options = {}) {
155
+ this.options = options;
156
+ }
157
+ async request(path, body, signal) {
158
+ signal?.throwIfAborted();
159
+ let response;
160
+ try {
161
+ response = await (this.options.fetch ?? fetch)(`${this.options.endpoint ?? 'https://1click.chaindefuser.com/v0'}${path}`, {
162
+ credentials: 'omit',
163
+ method: body === undefined ? 'GET' : 'POST',
164
+ ...(body === undefined
165
+ ? {}
166
+ : { headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) }),
167
+ signal: signal
168
+ ? AbortSignal.any([signal, AbortSignal.timeout(20000)])
169
+ : AbortSignal.timeout(20000),
170
+ });
171
+ }
172
+ catch {
173
+ signal?.throwIfAborted();
174
+ throw new TransportError();
175
+ }
176
+ if (response.status === 429 || response.status >= 500) {
177
+ const delay = parseRetryAfterMs(response.headers.get('retry-after'), this.now() * 1000) ?? 0;
178
+ throw new TransportError(Math.max(0, Math.min(300000, delay || 0)));
179
+ }
180
+ if (!response.ok) {
181
+ if (path === '/quote' && response.status >= 400 && response.status < 500) {
182
+ const body = await response.json().catch(() => null);
183
+ const message = body?.message;
184
+ throw new OneClickRefused(refusalMessage(message) ? message : 'The 1Click quote was refused.');
185
+ }
186
+ throw new Error(`1Click HTTP ${response.status}`);
187
+ }
188
+ const result = await response.json();
189
+ if (!result || typeof result !== 'object' || (Array.isArray(result) && path !== '/tokens'))
190
+ throw new Error('malformed 1Click response');
191
+ return result;
192
+ }
193
+ now() {
194
+ return (this.options.now ?? (() => Math.floor(Date.now() / 1000)))();
195
+ }
196
+ async tokens(signal) {
197
+ const result = await this.request('/tokens', undefined, signal);
198
+ if (!Array.isArray(result))
199
+ throw new Error('malformed token catalog');
200
+ return result.filter((t) => !!t &&
201
+ typeof t === 'object' &&
202
+ typeof t.assetId === 'string' &&
203
+ t.assetId.startsWith('nep141:') &&
204
+ !!t.assetId.slice(7) &&
205
+ typeof t.blockchain === 'string' &&
206
+ !!t.blockchain &&
207
+ typeof t.symbol === 'string' &&
208
+ Number.isInteger(t.decimals) &&
209
+ t.decimals >= 0 &&
210
+ t.decimals <= 255);
211
+ }
212
+ async preview(message, origin, options = {}) {
213
+ return this.quote(message, origin, { ...options, dry: true });
214
+ }
215
+ async quote(message, origin, options = {}) {
216
+ const p = parsePurchaseMessage(message);
217
+ if (p.route !== 'near-usdc' || p.asset !== NEAR_USDC)
218
+ throw new TypeError('1Click requires the NEAR USDC route');
219
+ const now = (this.options.now ?? (() => Math.floor(Date.now() / 1000)))();
220
+ const depositCutoff = Number(p.expires_at) - 600;
221
+ if (!Number.isSafeInteger(depositCutoff) || depositCutoff <= now)
222
+ throw new Error('purchase expired or insufficient delivery window');
223
+ validateOrigin(origin);
224
+ const refundType = origin.refundType ?? 'ORIGIN_CHAIN';
225
+ if (refundType === 'INTENTS')
226
+ account(origin.refundTo);
227
+ const tolerance = options.slippageTolerance ?? 100;
228
+ if (!Number.isInteger(tolerance) || tolerance < 0 || tolerance > 10000)
229
+ throw new TypeError('invalid slippage');
230
+ const request = {
231
+ dry: options.dry ?? false,
232
+ swapType: 'EXACT_OUTPUT',
233
+ slippageTolerance: tolerance,
234
+ originAsset: origin.asset,
235
+ depositType: 'ORIGIN_CHAIN',
236
+ ...(origin.depositMode ? { depositMode: origin.depositMode } : {}),
237
+ destinationAsset: `nep141:${NEAR_USDC}`,
238
+ amount: p.price,
239
+ refundTo: origin.refundTo,
240
+ refundType,
241
+ recipient: p.contract,
242
+ recipientType: 'DESTINATION_CHAIN',
243
+ deadline: new Date(depositCutoff * 1000).toISOString(),
244
+ customRecipientMsg: message,
245
+ };
246
+ const response = (await this.request('/quote', request, options.signal));
247
+ const preview = parseQuotePreview(request, response);
248
+ if (request.dry)
249
+ return preview;
250
+ const q = response.quote;
251
+ const deadline = typeof q.deadline === 'string' ? Date.parse(q.deadline) : NaN;
252
+ // The service's address-inactivity deadline is request.deadline + 72h in recorded
253
+ // quotes. It is not the buyer's cutoff. Never extend the echoed request deadline.
254
+ if (!Number.isFinite(deadline) || deadline < depositCutoff * 1000)
255
+ throw new Error('1Click deposit address deadline expires before the deposit cutoff');
256
+ if (typeof q.depositAddress !== 'string' || !q.depositAddress || q.depositAddress.length > 256)
257
+ throw new Error('quote has no deposit address');
258
+ const memo = q.depositMemo;
259
+ if ((origin.depositMode === 'MEMO' && (typeof memo !== 'string' || !memo)) ||
260
+ (memo !== undefined &&
261
+ (typeof memo !== 'string' ||
262
+ memo.length > 256 ||
263
+ Array.from(memo).some((c) => c.charCodeAt(0) < 32))))
264
+ throw new Error('invalid deposit memo');
265
+ const data = {
266
+ address: q.depositAddress,
267
+ amount: preview.amountIn,
268
+ asset: origin.asset,
269
+ chain: origin.chain,
270
+ ...(origin.blockchain ? { blockchain: origin.blockchain } : {}),
271
+ depositBefore: request.deadline,
272
+ purchaseExpiresAt: new Date(Number(p.expires_at) * 1000).toISOString(),
273
+ ...(memo === undefined ? {} : { memo }),
274
+ };
275
+ return { request, response, deposit: { ...data, qrData: JSON.stringify(data) } };
276
+ }
277
+ async status(depositAddress, options = {}) {
278
+ if (!depositAddress || depositAddress.length > 256)
279
+ throw new TypeError('invalid deposit address');
280
+ const result = (await this.request(`/status?depositAddress=${encodeURIComponent(depositAddress)}${options.memo === undefined ? '' : `&depositMemo=${encodeURIComponent(options.memo)}`}`, undefined, options.signal));
281
+ if (!states.includes(result.status))
282
+ throw new Error('unknown 1Click status');
283
+ return result;
284
+ }
285
+ async poll(address, options = {}) {
286
+ const max = options.maxPolls ?? 1200;
287
+ if (!Number.isInteger(max) || max < 1)
288
+ throw new TypeError('invalid poll count');
289
+ const ms = options.intervalMs ?? 3000;
290
+ if (!Number.isFinite(ms) || ms < 0 || ms > 30000)
291
+ throw new TypeError('invalid poll interval');
292
+ let checkpoint = options.checkpoint ?? {
293
+ address,
294
+ ...(options.memo === undefined ? {} : { memo: options.memo }),
295
+ attempts: 0,
296
+ errors: 0,
297
+ };
298
+ if (checkpoint.address !== address ||
299
+ checkpoint.memo !== options.memo ||
300
+ !Number.isSafeInteger(checkpoint.attempts) ||
301
+ checkpoint.attempts < 0 ||
302
+ !Number.isSafeInteger(checkpoint.errors) ||
303
+ checkpoint.errors < 0 ||
304
+ (checkpoint.nextPollAt !== undefined &&
305
+ (!Number.isFinite(checkpoint.nextPollAt) || checkpoint.nextPollAt < 0)))
306
+ throw new TypeError('invalid poll checkpoint');
307
+ options.signal?.throwIfAborted();
308
+ if (checkpoint.lastStatus &&
309
+ ['SUCCESS', 'REFUNDED', 'FAILED'].includes(checkpoint.lastStatus.status)) {
310
+ await options.onProgress?.(structuredClone(checkpoint));
311
+ return checkpoint.lastStatus;
312
+ }
313
+ if (checkpoint.nextPollAt && checkpoint.nextPollAt > this.now() * 1000)
314
+ await (this.options.wait ?? sleep)(Math.min(300000, checkpoint.nextPollAt - this.now() * 1000), options.signal);
315
+ for (let i = 0; i < max; i++) {
316
+ options.signal?.throwIfAborted();
317
+ let result;
318
+ let delay = ms;
319
+ try {
320
+ result = await this.status(address, options);
321
+ }
322
+ catch (error) {
323
+ if (!isTransient(error))
324
+ throw error;
325
+ delay = Math.max(Math.min(30000, ms * 2 ** Math.min(checkpoint.errors, 8)), error instanceof TransportError ? error.retryAfterMs : 0);
326
+ }
327
+ checkpoint = {
328
+ ...checkpoint,
329
+ attempts: checkpoint.attempts + 1,
330
+ errors: result ? 0 : checkpoint.errors + 1,
331
+ ...(result ? { lastStatus: result } : {}),
332
+ nextPollAt: this.now() * 1000 + delay,
333
+ };
334
+ await options.onProgress?.(structuredClone(checkpoint));
335
+ if (result && ['SUCCESS', 'REFUNDED', 'FAILED'].includes(result.status))
336
+ return result;
337
+ if (i + 1 < max)
338
+ await (this.options.wait ?? sleep)(delay, options.signal);
339
+ }
340
+ throw new AnnonError('settlement_pending', '1Click status still pending');
341
+ }
342
+ }