@absol-labs/agent 0.5.0 → 0.7.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.
- package/README.md +49 -10
- package/dist/discovery/registry.d.ts +260 -15
- package/dist/discovery/registry.d.ts.map +1 -1
- package/dist/discovery/registry.js +174 -31
- package/dist/discovery/registry.js.map +1 -1
- package/dist/frameworks/agentkit.js +1 -1
- package/dist/frameworks/agentkit.js.map +1 -1
- package/dist/frameworks/crewai.js +1 -1
- package/dist/frameworks/crewai.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +3 -1
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +9 -7
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +3 -1
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +15 -1
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/sdk/invoke.d.ts +16 -2
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +117 -1
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.d.ts +97 -0
- package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -0
- package/dist/wallet/autonomous-wallet-broker.js +468 -0
- package/dist/wallet/autonomous-wallet-broker.js.map +1 -0
- package/dist/wallet/autonomous-wallet-protocol.d.ts +56 -0
- package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -0
- package/dist/wallet/autonomous-wallet-protocol.js +15 -0
- package/dist/wallet/autonomous-wallet-protocol.js.map +1 -0
- package/dist/wallet/autonomous-wallet.d.ts +106 -0
- package/dist/wallet/autonomous-wallet.d.ts.map +1 -0
- package/dist/wallet/autonomous-wallet.js +494 -0
- package/dist/wallet/autonomous-wallet.js.map +1 -0
- package/dist/wallet/privy-broker-server-entry.d.ts +2 -0
- package/dist/wallet/privy-broker-server-entry.d.ts.map +1 -0
- package/dist/wallet/privy-broker-server-entry.js +8 -0
- package/dist/wallet/privy-broker-server-entry.js.map +1 -0
- package/dist/wallet/privy-broker-server.d.ts +29 -0
- package/dist/wallet/privy-broker-server.d.ts.map +1 -0
- package/dist/wallet/privy-broker-server.js +480 -0
- package/dist/wallet/privy-broker-server.js.map +1 -0
- package/dist/wallet/privy-session-broker.d.ts +109 -0
- package/dist/wallet/privy-session-broker.d.ts.map +1 -0
- package/dist/wallet/privy-session-broker.js +372 -0
- package/dist/wallet/privy-session-broker.js.map +1 -0
- package/dist/wallet/privy-session-provider.d.ts +21 -0
- package/dist/wallet/privy-session-provider.d.ts.map +1 -0
- package/dist/wallet/privy-session-provider.js +94 -0
- package/dist/wallet/privy-session-provider.js.map +1 -0
- package/dist/wallet/provider.d.ts +51 -2
- package/dist/wallet/provider.d.ts.map +1 -1
- package/dist/wallet/provider.js +138 -1
- package/dist/wallet/provider.js.map +1 -1
- package/package.json +4 -2
- package/src/discovery/registry.ts +213 -35
- package/src/frameworks/agentkit.ts +1 -1
- package/src/frameworks/crewai.ts +1 -1
- package/src/gateway/caller-auth-gateway.ts +13 -9
- package/src/gateway/http-server.ts +18 -1
- package/src/index.ts +78 -0
- package/src/mcp/server.ts +2 -0
- package/src/sdk/invoke.ts +186 -4
- package/src/wallet/autonomous-wallet-broker.ts +764 -0
- package/src/wallet/autonomous-wallet-protocol.ts +71 -0
- package/src/wallet/autonomous-wallet.ts +779 -0
- package/src/wallet/privy-broker-server-entry.ts +9 -0
- package/src/wallet/privy-broker-server.ts +573 -0
- package/src/wallet/privy-session-broker.ts +634 -0
- package/src/wallet/privy-session-provider.ts +129 -0
- package/src/wallet/provider.ts +260 -3
|
@@ -0,0 +1,779 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
generateAuthorizationSignature,
|
|
5
|
+
generateP256KeyPair,
|
|
6
|
+
} from "@privy-io/node";
|
|
7
|
+
import { getAddress, type Address, type Hex } from "viem";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createPrivyEmbeddedAccount,
|
|
11
|
+
type PrivyEip1193Provider,
|
|
12
|
+
type PrivyEmbeddedWalletConfig,
|
|
13
|
+
} from "./provider.js";
|
|
14
|
+
import {
|
|
15
|
+
AUTONOMOUS_WALLET_CHAIN_ID,
|
|
16
|
+
AUTONOMOUS_WALLET_VERSION,
|
|
17
|
+
autonomousProofBytes,
|
|
18
|
+
type AutonomousWalletBroker,
|
|
19
|
+
type AutonomousWalletProof,
|
|
20
|
+
type PreparedPrivyRequest,
|
|
21
|
+
} from "./autonomous-wallet-protocol.js";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
AUTONOMOUS_WALLET_CAIP2,
|
|
25
|
+
AUTONOMOUS_WALLET_CHAIN_ID,
|
|
26
|
+
AUTONOMOUS_WALLET_VERSION,
|
|
27
|
+
autonomousProofBytes,
|
|
28
|
+
type AutonomousWalletBroker,
|
|
29
|
+
type AutonomousWalletProof,
|
|
30
|
+
type PreparedPrivyRequest,
|
|
31
|
+
} from "./autonomous-wallet-protocol.js";
|
|
32
|
+
const KEYCHAIN_SERVICE = "com.absol-labs.metrik.autonomous-wallet";
|
|
33
|
+
export const METRIK_AUTONOMOUS_WALLET_BROKER_URL =
|
|
34
|
+
"https://wallet.137.23.50.249.sslip.io";
|
|
35
|
+
|
|
36
|
+
export interface AutonomousWalletRecord {
|
|
37
|
+
readonly version: typeof AUTONOMOUS_WALLET_VERSION;
|
|
38
|
+
readonly appId: string;
|
|
39
|
+
readonly brokerUrl: string;
|
|
40
|
+
readonly publicKey: string;
|
|
41
|
+
/** Base64 PKCS8 P-256 key. This field must only be stored in a secure store. */
|
|
42
|
+
readonly privateKey: string;
|
|
43
|
+
readonly provisioningNonce: string;
|
|
44
|
+
readonly walletId?: string;
|
|
45
|
+
readonly address?: Address;
|
|
46
|
+
readonly policyId?: string;
|
|
47
|
+
readonly createdAt: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface AutonomousWalletCredentialStore {
|
|
51
|
+
load(key: string): Promise<AutonomousWalletRecord | null>;
|
|
52
|
+
save(key: string, record: AutonomousWalletRecord): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class AutonomousWalletError extends Error {
|
|
56
|
+
constructor(
|
|
57
|
+
readonly code:
|
|
58
|
+
| "invalid-config"
|
|
59
|
+
| "store-failure"
|
|
60
|
+
| "broker-failure"
|
|
61
|
+
| "proof-failure"
|
|
62
|
+
| "invalid-response",
|
|
63
|
+
message: string,
|
|
64
|
+
options?: { readonly cause?: unknown },
|
|
65
|
+
) {
|
|
66
|
+
super(message, options);
|
|
67
|
+
this.name = "AutonomousWalletError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Wire adapter for the broker HTTP contract. The only credential sent by this
|
|
73
|
+
* client is the P-256 proof/signature; the PKCS8 private key never enters a
|
|
74
|
+
* request body or header.
|
|
75
|
+
*/
|
|
76
|
+
export class HttpAutonomousWalletBroker implements AutonomousWalletBroker {
|
|
77
|
+
private readonly baseUrl: string;
|
|
78
|
+
private readonly fetchImpl: typeof fetch;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
brokerUrl: string,
|
|
82
|
+
options: { readonly fetchImpl?: typeof fetch } = {},
|
|
83
|
+
) {
|
|
84
|
+
const url = new URL(brokerUrl);
|
|
85
|
+
if (url.protocol !== "https:" && url.hostname !== "localhost") {
|
|
86
|
+
throw new AutonomousWalletError(
|
|
87
|
+
"invalid-config",
|
|
88
|
+
"autonomous wallet broker URL must use HTTPS (except localhost development)",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
url.search = "";
|
|
92
|
+
url.hash = "";
|
|
93
|
+
this.baseUrl = url.toString().replace(/\/$/, "");
|
|
94
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async provision(input: Parameters<AutonomousWalletBroker["provision"]>[0]) {
|
|
98
|
+
return validateProvisionResponse(
|
|
99
|
+
await this.post("/v1/autonomous-wallets", input),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async prepare(input: Parameters<AutonomousWalletBroker["prepare"]>[0]) {
|
|
104
|
+
return validatePreparedResponse(
|
|
105
|
+
await this.post("/v1/autonomous-wallets/prepare", input),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async execute(input: Parameters<AutonomousWalletBroker["execute"]>[0]) {
|
|
110
|
+
const response = await this.post("/v1/autonomous-wallets/execute", input);
|
|
111
|
+
if (
|
|
112
|
+
!isRecord(response) ||
|
|
113
|
+
typeof response.result !== "string" ||
|
|
114
|
+
!/^0x[0-9a-fA-F]+$/.test(response.result)
|
|
115
|
+
) {
|
|
116
|
+
throw new AutonomousWalletError(
|
|
117
|
+
"invalid-response",
|
|
118
|
+
"autonomous wallet broker returned an invalid execution result",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return response.result as Hex;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async post(path: string, body: unknown): Promise<unknown> {
|
|
125
|
+
let response: Response;
|
|
126
|
+
try {
|
|
127
|
+
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "content-type": "application/json" },
|
|
130
|
+
body: JSON.stringify(body, bigintJson),
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw new AutonomousWalletError(
|
|
134
|
+
"broker-failure",
|
|
135
|
+
"autonomous wallet broker request failed",
|
|
136
|
+
{ cause: error },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const payload = (await response.json().catch(() => null)) as {
|
|
140
|
+
readonly error?: unknown;
|
|
141
|
+
} | null;
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const detail =
|
|
144
|
+
typeof payload?.error === "string" ? `: ${payload.error}` : "";
|
|
145
|
+
throw new AutonomousWalletError(
|
|
146
|
+
"broker-failure",
|
|
147
|
+
`autonomous wallet broker rejected request (${response.status})${detail}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (payload === null || typeof payload !== "object") {
|
|
151
|
+
throw new AutonomousWalletError(
|
|
152
|
+
"invalid-response",
|
|
153
|
+
"autonomous wallet broker returned invalid JSON",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return payload;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ProvisionAutonomousWalletOptions {
|
|
161
|
+
readonly appId: string;
|
|
162
|
+
readonly policyId: string;
|
|
163
|
+
readonly brokerUrl: string;
|
|
164
|
+
readonly storeKey: string;
|
|
165
|
+
readonly broker: AutonomousWalletBroker;
|
|
166
|
+
readonly store?: AutonomousWalletCredentialStore;
|
|
167
|
+
readonly nowSeconds?: number;
|
|
168
|
+
readonly nonce?: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface ProvisionMetrikAutonomousWalletOptions {
|
|
172
|
+
readonly storeKey: string;
|
|
173
|
+
readonly brokerUrl?: string;
|
|
174
|
+
readonly store?: AutonomousWalletCredentialStore;
|
|
175
|
+
readonly fetchImpl?: typeof fetch;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Zero-secret Metrik entry point. Public app/policy configuration is fetched
|
|
180
|
+
* from the trusted HTTPS broker; the authorization key is generated and kept
|
|
181
|
+
* in the local OS credential store.
|
|
182
|
+
*/
|
|
183
|
+
export async function provisionMetrikAutonomousWallet(
|
|
184
|
+
options: ProvisionMetrikAutonomousWalletOptions,
|
|
185
|
+
): Promise<AutonomousWalletRecord> {
|
|
186
|
+
const brokerUrl = options.brokerUrl ?? METRIK_AUTONOMOUS_WALLET_BROKER_URL;
|
|
187
|
+
const base = normalizeBrokerUrl(brokerUrl);
|
|
188
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
189
|
+
const response = await fetchImpl(`${base}/v1/config`, {
|
|
190
|
+
headers: { accept: "application/json" },
|
|
191
|
+
}).catch((error: unknown) => {
|
|
192
|
+
throw new AutonomousWalletError(
|
|
193
|
+
"broker-failure",
|
|
194
|
+
"autonomous wallet broker configuration is unreachable",
|
|
195
|
+
{ cause: error },
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
const raw = (await response.json().catch(() => null)) as unknown;
|
|
199
|
+
if (!response.ok || !isRecord(raw))
|
|
200
|
+
throw new AutonomousWalletError(
|
|
201
|
+
"broker-failure",
|
|
202
|
+
`autonomous wallet broker rejected configuration (${response.status})`,
|
|
203
|
+
);
|
|
204
|
+
const appId = raw.appId;
|
|
205
|
+
const policyId = raw.policyId;
|
|
206
|
+
if (
|
|
207
|
+
typeof appId !== "string" ||
|
|
208
|
+
appId.trim() === "" ||
|
|
209
|
+
typeof policyId !== "string" ||
|
|
210
|
+
policyId.trim() === "" ||
|
|
211
|
+
raw.chainId !== AUTONOMOUS_WALLET_CHAIN_ID
|
|
212
|
+
) {
|
|
213
|
+
throw new AutonomousWalletError(
|
|
214
|
+
"invalid-response",
|
|
215
|
+
"autonomous wallet broker returned invalid public configuration",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const broker = new HttpAutonomousWalletBroker(base, { fetchImpl });
|
|
219
|
+
return provisionAutonomousWallet({
|
|
220
|
+
appId,
|
|
221
|
+
policyId,
|
|
222
|
+
brokerUrl: base,
|
|
223
|
+
storeKey: options.storeKey,
|
|
224
|
+
broker,
|
|
225
|
+
...(options.store === undefined ? {} : { store: options.store }),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Creates or restores an agent-owned Privy wallet. A pending record is saved
|
|
231
|
+
* before the broker call so a network failure can retry with the same key and
|
|
232
|
+
* idempotency key. If secure storage fails, provisioning stops before any
|
|
233
|
+
* broker request is made.
|
|
234
|
+
*/
|
|
235
|
+
export async function provisionAutonomousWallet(
|
|
236
|
+
options: ProvisionAutonomousWalletOptions,
|
|
237
|
+
): Promise<AutonomousWalletRecord> {
|
|
238
|
+
validateProvisionOptions(options);
|
|
239
|
+
const store = options.store ?? createDefaultAutonomousWalletStore();
|
|
240
|
+
const existingRaw = await loadRecord(store, options.storeKey);
|
|
241
|
+
if (existingRaw !== null) {
|
|
242
|
+
const existing = validateStoredRecord(existingRaw);
|
|
243
|
+
validateExistingRecord(existing, options);
|
|
244
|
+
if (existing.walletId === undefined || existing.address === undefined) {
|
|
245
|
+
return provisionPendingRecord(existing, options, store);
|
|
246
|
+
}
|
|
247
|
+
return existing;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const keyPair = await generateP256KeyPair();
|
|
251
|
+
const now = options.nowSeconds ?? Math.floor(Date.now() / 1_000);
|
|
252
|
+
const nonce = options.nonce ?? cryptoRandomNonce();
|
|
253
|
+
const pending: AutonomousWalletRecord = {
|
|
254
|
+
version: AUTONOMOUS_WALLET_VERSION,
|
|
255
|
+
appId: options.appId,
|
|
256
|
+
brokerUrl: normalizeBrokerUrl(options.brokerUrl),
|
|
257
|
+
publicKey: keyPair.publicKey,
|
|
258
|
+
privateKey: keyPair.privateKey,
|
|
259
|
+
provisioningNonce: nonce,
|
|
260
|
+
createdAt: now,
|
|
261
|
+
};
|
|
262
|
+
await saveRecord(store, options.storeKey, pending);
|
|
263
|
+
return provisionPendingRecord(pending, options, store);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function provisionPendingRecord(
|
|
267
|
+
pending: AutonomousWalletRecord,
|
|
268
|
+
options: ProvisionAutonomousWalletOptions,
|
|
269
|
+
store: AutonomousWalletCredentialStore,
|
|
270
|
+
): Promise<AutonomousWalletRecord> {
|
|
271
|
+
const proofInput = {
|
|
272
|
+
version: pending.version,
|
|
273
|
+
appId: pending.appId,
|
|
274
|
+
chainId: AUTONOMOUS_WALLET_CHAIN_ID,
|
|
275
|
+
publicKey: pending.publicKey,
|
|
276
|
+
nonce: pending.provisioningNonce,
|
|
277
|
+
// A retry gets a fresh proof timestamp while retaining the original key
|
|
278
|
+
// and idempotency nonce. This permits recovery after a transient outage.
|
|
279
|
+
issuedAt: options.nowSeconds ?? Math.floor(Date.now() / 1_000),
|
|
280
|
+
} as const;
|
|
281
|
+
const proof: AutonomousWalletProof = {
|
|
282
|
+
...proofInput,
|
|
283
|
+
signature: generateAuthorizationSignature({
|
|
284
|
+
authorizationPrivateKey: pending.privateKey,
|
|
285
|
+
input: autonomousProofBytes(proofInput),
|
|
286
|
+
}),
|
|
287
|
+
};
|
|
288
|
+
let response: Awaited<ReturnType<AutonomousWalletBroker["provision"]>>;
|
|
289
|
+
try {
|
|
290
|
+
response = await options.broker.provision({
|
|
291
|
+
appId: options.appId,
|
|
292
|
+
chainId: AUTONOMOUS_WALLET_CHAIN_ID,
|
|
293
|
+
publicKey: pending.publicKey,
|
|
294
|
+
proof,
|
|
295
|
+
idempotencyKey: pending.provisioningNonce,
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
throw new AutonomousWalletError(
|
|
299
|
+
"broker-failure",
|
|
300
|
+
"autonomous wallet broker rejected provisioning; the pending key remains for safe retry",
|
|
301
|
+
{ cause: error },
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const validated = validateProvisionResponse(response);
|
|
305
|
+
if (validated.policyId !== options.policyId) {
|
|
306
|
+
throw new AutonomousWalletError(
|
|
307
|
+
"invalid-response",
|
|
308
|
+
"autonomous wallet broker returned an unexpected policy id",
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const ready: AutonomousWalletRecord = {
|
|
312
|
+
...pending,
|
|
313
|
+
walletId: validated.walletId,
|
|
314
|
+
address: getAddress(validated.address),
|
|
315
|
+
policyId: validated.policyId,
|
|
316
|
+
};
|
|
317
|
+
await saveRecord(store, options.storeKey, ready);
|
|
318
|
+
return ready;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export interface CreateAutonomousWalletProviderOptions {
|
|
322
|
+
readonly wallet: AutonomousWalletRecord;
|
|
323
|
+
readonly broker: AutonomousWalletBroker;
|
|
324
|
+
readonly rpcUrl?: string;
|
|
325
|
+
readonly fetchImpl?: typeof fetch;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Builds the provider used by the existing Privy EOA adapter. Reads use the
|
|
330
|
+
* public Base Sepolia RPC; writes are prepared by the broker, signed locally
|
|
331
|
+
* with the agent key, and then executed once by the broker.
|
|
332
|
+
*/
|
|
333
|
+
export function createAutonomousWalletProvider(
|
|
334
|
+
options: CreateAutonomousWalletProviderOptions,
|
|
335
|
+
): PrivyEip1193Provider {
|
|
336
|
+
if (
|
|
337
|
+
options.wallet.walletId === undefined ||
|
|
338
|
+
options.wallet.address === undefined
|
|
339
|
+
) {
|
|
340
|
+
throw new AutonomousWalletError(
|
|
341
|
+
"invalid-config",
|
|
342
|
+
"cannot create a provider before autonomous wallet provisioning completes",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const address = options.wallet.address;
|
|
346
|
+
let rpcId = 0;
|
|
347
|
+
return {
|
|
348
|
+
async request({ method, params = [] }) {
|
|
349
|
+
if (method === "eth_chainId") return "0x14a34";
|
|
350
|
+
if (method === "eth_accounts" || method === "eth_requestAccounts")
|
|
351
|
+
return [address];
|
|
352
|
+
if (
|
|
353
|
+
method === "eth_sendTransaction" ||
|
|
354
|
+
method === "eth_signTypedData_v4"
|
|
355
|
+
) {
|
|
356
|
+
const prepared = await options.broker.prepare({
|
|
357
|
+
walletId: options.wallet.walletId!,
|
|
358
|
+
publicKey: options.wallet.publicKey,
|
|
359
|
+
method,
|
|
360
|
+
params,
|
|
361
|
+
});
|
|
362
|
+
if (
|
|
363
|
+
prepared.walletId !== options.wallet.walletId ||
|
|
364
|
+
prepared.method !== method
|
|
365
|
+
) {
|
|
366
|
+
throw new AutonomousWalletError(
|
|
367
|
+
"invalid-response",
|
|
368
|
+
"autonomous broker returned a mismatched prepared request",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (Math.floor(Date.now() / 1_000) >= prepared.expiresAt) {
|
|
372
|
+
throw new AutonomousWalletError(
|
|
373
|
+
"broker-failure",
|
|
374
|
+
"autonomous broker returned an expired prepared request",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const signature = generateAuthorizationSignature({
|
|
378
|
+
authorizationPrivateKey: options.wallet.privateKey,
|
|
379
|
+
input: decodeBase64(prepared.payload),
|
|
380
|
+
});
|
|
381
|
+
return options.broker.execute({
|
|
382
|
+
requestId: prepared.requestId,
|
|
383
|
+
publicKey: options.wallet.publicKey,
|
|
384
|
+
signature,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const response = await (
|
|
388
|
+
options.fetchImpl ?? globalThis.fetch.bind(globalThis)
|
|
389
|
+
)(options.rpcUrl ?? "https://base-sepolia-rpc.publicnode.com", {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: { "content-type": "application/json" },
|
|
392
|
+
body: JSON.stringify(
|
|
393
|
+
{
|
|
394
|
+
jsonrpc: "2.0",
|
|
395
|
+
id: ++rpcId,
|
|
396
|
+
method,
|
|
397
|
+
params,
|
|
398
|
+
},
|
|
399
|
+
bigintJson,
|
|
400
|
+
),
|
|
401
|
+
});
|
|
402
|
+
const body = (await response.json()) as {
|
|
403
|
+
readonly result?: unknown;
|
|
404
|
+
readonly error?: { readonly message?: string };
|
|
405
|
+
};
|
|
406
|
+
if (!response.ok || body.error !== undefined) {
|
|
407
|
+
throw new AutonomousWalletError(
|
|
408
|
+
"broker-failure",
|
|
409
|
+
body.error?.message ?? `Base Sepolia RPC rejected ${method}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return body.result;
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function createAutonomousWalletAccount(
|
|
418
|
+
options: CreateAutonomousWalletProviderOptions,
|
|
419
|
+
) {
|
|
420
|
+
const provider = createAutonomousWalletProvider(options);
|
|
421
|
+
const config: PrivyEmbeddedWalletConfig = {
|
|
422
|
+
appId: options.wallet.appId,
|
|
423
|
+
address: options.wallet.address!,
|
|
424
|
+
provider,
|
|
425
|
+
chainId: AUTONOMOUS_WALLET_CHAIN_ID,
|
|
426
|
+
};
|
|
427
|
+
return createPrivyEmbeddedAccount(config);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export class InMemoryAutonomousWalletStore implements AutonomousWalletCredentialStore {
|
|
431
|
+
private readonly records = new Map<string, AutonomousWalletRecord>();
|
|
432
|
+
|
|
433
|
+
async load(key: string): Promise<AutonomousWalletRecord | null> {
|
|
434
|
+
return this.records.get(key) ?? null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async save(key: string, record: AutonomousWalletRecord): Promise<void> {
|
|
438
|
+
this.records.set(key, record);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Uses the host OS credential manager. There is intentionally no plaintext
|
|
444
|
+
* file fallback: unsupported hosts receive an actionable error instead.
|
|
445
|
+
*/
|
|
446
|
+
export class OsCredentialStore implements AutonomousWalletCredentialStore {
|
|
447
|
+
async load(key: string): Promise<AutonomousWalletRecord | null> {
|
|
448
|
+
const raw = await this.readSecret(key);
|
|
449
|
+
if (raw === null) return null;
|
|
450
|
+
try {
|
|
451
|
+
return validateStoredRecord(JSON.parse(raw));
|
|
452
|
+
} catch (error) {
|
|
453
|
+
throw new AutonomousWalletError(
|
|
454
|
+
"store-failure",
|
|
455
|
+
"OS credential contains invalid wallet data",
|
|
456
|
+
{ cause: error },
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async save(key: string, record: AutonomousWalletRecord): Promise<void> {
|
|
462
|
+
const raw = JSON.stringify(record);
|
|
463
|
+
try {
|
|
464
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
465
|
+
await new AsyncEntry(KEYCHAIN_SERVICE, key).setPassword(raw);
|
|
466
|
+
return;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
throw new AutonomousWalletError(
|
|
469
|
+
"store-failure",
|
|
470
|
+
"OS credential store rejected the autonomous wallet record; no plaintext fallback is allowed",
|
|
471
|
+
{ cause: error },
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private async readSecret(key: string): Promise<string | null> {
|
|
477
|
+
try {
|
|
478
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
479
|
+
return (
|
|
480
|
+
(await new AsyncEntry(KEYCHAIN_SERVICE, key).getPassword()) ?? null
|
|
481
|
+
);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
throw new AutonomousWalletError(
|
|
484
|
+
"store-failure",
|
|
485
|
+
"OS credential store could not read the autonomous wallet record",
|
|
486
|
+
{ cause: error },
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function createDefaultAutonomousWalletStore(): AutonomousWalletCredentialStore {
|
|
493
|
+
return new OsCredentialStore();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function loadRecord(
|
|
497
|
+
store: AutonomousWalletCredentialStore,
|
|
498
|
+
key: string,
|
|
499
|
+
): Promise<AutonomousWalletRecord | null> {
|
|
500
|
+
try {
|
|
501
|
+
return await store.load(key);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (error instanceof AutonomousWalletError) throw error;
|
|
504
|
+
throw new AutonomousWalletError(
|
|
505
|
+
"store-failure",
|
|
506
|
+
"secure credential store could not be read",
|
|
507
|
+
{ cause: error },
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function saveRecord(
|
|
513
|
+
store: AutonomousWalletCredentialStore,
|
|
514
|
+
key: string,
|
|
515
|
+
record: AutonomousWalletRecord,
|
|
516
|
+
): Promise<void> {
|
|
517
|
+
try {
|
|
518
|
+
await store.save(key, record);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (error instanceof AutonomousWalletError) throw error;
|
|
521
|
+
throw new AutonomousWalletError(
|
|
522
|
+
"store-failure",
|
|
523
|
+
"secure credential store could not persist the autonomous wallet key",
|
|
524
|
+
{ cause: error },
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function validateProvisionOptions(
|
|
530
|
+
options: ProvisionAutonomousWalletOptions,
|
|
531
|
+
): void {
|
|
532
|
+
if (options.appId.trim() === "")
|
|
533
|
+
throw new AutonomousWalletError(
|
|
534
|
+
"invalid-config",
|
|
535
|
+
"Privy public app id is required",
|
|
536
|
+
);
|
|
537
|
+
if (options.policyId.trim() === "")
|
|
538
|
+
throw new AutonomousWalletError(
|
|
539
|
+
"invalid-config",
|
|
540
|
+
"Metrik autonomous wallet policy id is required",
|
|
541
|
+
);
|
|
542
|
+
let broker: URL;
|
|
543
|
+
try {
|
|
544
|
+
broker = new URL(options.brokerUrl);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
throw new AutonomousWalletError(
|
|
547
|
+
"invalid-config",
|
|
548
|
+
"autonomous wallet broker URL is invalid",
|
|
549
|
+
{ cause: error },
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
if (broker.protocol !== "https:" && broker.hostname !== "localhost")
|
|
553
|
+
throw new AutonomousWalletError(
|
|
554
|
+
"invalid-config",
|
|
555
|
+
"autonomous wallet broker URL must use HTTPS (except localhost development)",
|
|
556
|
+
);
|
|
557
|
+
if (options.storeKey.trim() === "")
|
|
558
|
+
throw new AutonomousWalletError(
|
|
559
|
+
"invalid-config",
|
|
560
|
+
"secure store key is required",
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function validateExistingRecord(
|
|
565
|
+
record: AutonomousWalletRecord,
|
|
566
|
+
options: ProvisionAutonomousWalletOptions,
|
|
567
|
+
): void {
|
|
568
|
+
if (
|
|
569
|
+
record.version !== AUTONOMOUS_WALLET_VERSION ||
|
|
570
|
+
record.appId !== options.appId ||
|
|
571
|
+
record.brokerUrl !== normalizeBrokerUrl(options.brokerUrl) ||
|
|
572
|
+
(record.policyId !== undefined && record.policyId !== options.policyId)
|
|
573
|
+
) {
|
|
574
|
+
throw new AutonomousWalletError(
|
|
575
|
+
"invalid-config",
|
|
576
|
+
"stored autonomous wallet belongs to a different app or broker",
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function validateStoredRecord(value: unknown): AutonomousWalletRecord {
|
|
582
|
+
if (
|
|
583
|
+
!isRecord(value) ||
|
|
584
|
+
value.version !== AUTONOMOUS_WALLET_VERSION ||
|
|
585
|
+
typeof value.appId !== "string" ||
|
|
586
|
+
value.appId.trim() === "" ||
|
|
587
|
+
typeof value.brokerUrl !== "string" ||
|
|
588
|
+
typeof value.publicKey !== "string" ||
|
|
589
|
+
value.publicKey.trim() === "" ||
|
|
590
|
+
typeof value.privateKey !== "string" ||
|
|
591
|
+
value.privateKey.trim() === "" ||
|
|
592
|
+
typeof value.provisioningNonce !== "string" ||
|
|
593
|
+
value.provisioningNonce.trim() === "" ||
|
|
594
|
+
typeof value.createdAt !== "number" ||
|
|
595
|
+
!Number.isSafeInteger(value.createdAt)
|
|
596
|
+
) {
|
|
597
|
+
throw new AutonomousWalletError(
|
|
598
|
+
"store-failure",
|
|
599
|
+
"OS credential contains an invalid autonomous wallet record",
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (
|
|
603
|
+
value.walletId !== undefined &&
|
|
604
|
+
(typeof value.walletId !== "string" || value.walletId.trim() === "")
|
|
605
|
+
) {
|
|
606
|
+
throw new AutonomousWalletError(
|
|
607
|
+
"store-failure",
|
|
608
|
+
"OS credential contains an invalid wallet id",
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
if (
|
|
612
|
+
value.policyId !== undefined &&
|
|
613
|
+
(typeof value.policyId !== "string" || value.policyId.trim() === "")
|
|
614
|
+
) {
|
|
615
|
+
throw new AutonomousWalletError(
|
|
616
|
+
"store-failure",
|
|
617
|
+
"OS credential contains an invalid policy id",
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (
|
|
621
|
+
value.address !== undefined &&
|
|
622
|
+
(typeof value.address !== "string" ||
|
|
623
|
+
!/^0x[0-9a-fA-F]{40}$/.test(value.address))
|
|
624
|
+
) {
|
|
625
|
+
throw new AutonomousWalletError(
|
|
626
|
+
"store-failure",
|
|
627
|
+
"OS credential contains an invalid wallet address",
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
validateP256KeyPair(value.publicKey, value.privateKey);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
throw new AutonomousWalletError(
|
|
634
|
+
"store-failure",
|
|
635
|
+
"OS credential contains invalid P-256 key material",
|
|
636
|
+
{ cause: error },
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
version: AUTONOMOUS_WALLET_VERSION,
|
|
641
|
+
appId: value.appId,
|
|
642
|
+
brokerUrl: value.brokerUrl,
|
|
643
|
+
publicKey: value.publicKey,
|
|
644
|
+
privateKey: value.privateKey,
|
|
645
|
+
provisioningNonce: value.provisioningNonce,
|
|
646
|
+
...(value.walletId === undefined ? {} : { walletId: value.walletId }),
|
|
647
|
+
...(value.address === undefined
|
|
648
|
+
? {}
|
|
649
|
+
: { address: getAddress(value.address) }),
|
|
650
|
+
...(value.policyId === undefined ? {} : { policyId: value.policyId }),
|
|
651
|
+
createdAt: value.createdAt,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function normalizeBrokerUrl(value: string): string {
|
|
656
|
+
const url = new URL(value);
|
|
657
|
+
url.search = "";
|
|
658
|
+
url.hash = "";
|
|
659
|
+
return url.toString().replace(/\/$/, "");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function cryptoRandomNonce(): string {
|
|
663
|
+
const bytes = new Uint8Array(24);
|
|
664
|
+
crypto.getRandomValues(bytes);
|
|
665
|
+
return Buffer.from(bytes).toString("base64url");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function decodeBase64(value: string): Uint8Array {
|
|
669
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 === 1) {
|
|
670
|
+
throw new AutonomousWalletError(
|
|
671
|
+
"invalid-response",
|
|
672
|
+
"broker returned invalid proof payload encoding",
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const decoded = Buffer.from(value, "base64");
|
|
676
|
+
if (decoded.length === 0 || decoded.toString("base64") !== value) {
|
|
677
|
+
throw new AutonomousWalletError(
|
|
678
|
+
"invalid-response",
|
|
679
|
+
"broker returned an empty proof payload",
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return new Uint8Array(decoded);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function validateP256KeyPair(publicKey: string, privateKey: string): void {
|
|
686
|
+
const publicObject = createPublicKey({
|
|
687
|
+
key: Buffer.from(decodeBase64(publicKey)),
|
|
688
|
+
format: "der",
|
|
689
|
+
type: "spki",
|
|
690
|
+
});
|
|
691
|
+
const privateObject = createPrivateKey({
|
|
692
|
+
key: Buffer.from(decodeBase64(privateKey)),
|
|
693
|
+
format: "der",
|
|
694
|
+
type: "pkcs8",
|
|
695
|
+
});
|
|
696
|
+
if (
|
|
697
|
+
publicObject.asymmetricKeyType !== "ec" ||
|
|
698
|
+
publicObject.asymmetricKeyDetails?.namedCurve !== "prime256v1" ||
|
|
699
|
+
privateObject.asymmetricKeyType !== "ec" ||
|
|
700
|
+
privateObject.asymmetricKeyDetails?.namedCurve !== "prime256v1"
|
|
701
|
+
) {
|
|
702
|
+
throw new Error("key material is not P-256");
|
|
703
|
+
}
|
|
704
|
+
const derived = createPublicKey(privateObject)
|
|
705
|
+
.export({ format: "der", type: "spki" })
|
|
706
|
+
.toString("base64");
|
|
707
|
+
if (derived !== publicKey)
|
|
708
|
+
throw new Error("public key does not match private key");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function validateProvisionResponse(value: unknown): {
|
|
712
|
+
readonly walletId: string;
|
|
713
|
+
readonly address: Address;
|
|
714
|
+
readonly policyId: string;
|
|
715
|
+
} {
|
|
716
|
+
if (
|
|
717
|
+
!isRecord(value) ||
|
|
718
|
+
typeof value.walletId !== "string" ||
|
|
719
|
+
value.walletId.trim() === "" ||
|
|
720
|
+
typeof value.address !== "string" ||
|
|
721
|
+
typeof value.policyId !== "string" ||
|
|
722
|
+
value.policyId.trim() === ""
|
|
723
|
+
) {
|
|
724
|
+
throw new AutonomousWalletError(
|
|
725
|
+
"invalid-response",
|
|
726
|
+
"autonomous wallet broker returned an invalid wallet record",
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
return {
|
|
731
|
+
walletId: value.walletId,
|
|
732
|
+
address: getAddress(value.address),
|
|
733
|
+
policyId: value.policyId,
|
|
734
|
+
};
|
|
735
|
+
} catch (error) {
|
|
736
|
+
throw new AutonomousWalletError(
|
|
737
|
+
"invalid-response",
|
|
738
|
+
"autonomous wallet broker returned an invalid wallet address",
|
|
739
|
+
{ cause: error },
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function validatePreparedResponse(value: unknown): PreparedPrivyRequest {
|
|
745
|
+
if (
|
|
746
|
+
!isRecord(value) ||
|
|
747
|
+
typeof value.requestId !== "string" ||
|
|
748
|
+
value.requestId.trim() === "" ||
|
|
749
|
+
typeof value.walletId !== "string" ||
|
|
750
|
+
value.walletId.trim() === "" ||
|
|
751
|
+
(value.method !== "eth_sendTransaction" &&
|
|
752
|
+
value.method !== "eth_signTypedData_v4") ||
|
|
753
|
+
typeof value.payload !== "string" ||
|
|
754
|
+
value.payload.length === 0 ||
|
|
755
|
+
typeof value.expiresAt !== "number" ||
|
|
756
|
+
!Number.isSafeInteger(value.expiresAt)
|
|
757
|
+
) {
|
|
758
|
+
throw new AutonomousWalletError(
|
|
759
|
+
"invalid-response",
|
|
760
|
+
"autonomous wallet broker returned an invalid prepared request",
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
decodeBase64(value.payload);
|
|
764
|
+
return {
|
|
765
|
+
requestId: value.requestId,
|
|
766
|
+
walletId: value.walletId,
|
|
767
|
+
method: value.method,
|
|
768
|
+
payload: value.payload,
|
|
769
|
+
expiresAt: value.expiresAt,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
774
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function bigintJson(_key: string, value: unknown): unknown {
|
|
778
|
+
return typeof value === "bigint" ? `0x${value.toString(16)}` : value;
|
|
779
|
+
}
|