nodpay 0.2.34 → 0.2.36
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/LICENSE +21 -0
- package/SKILL.md +2 -2
- package/package.json +2 -2
- package/scripts/env.mjs +59 -0
- package/scripts/keygen.mjs +10 -19
- package/scripts/propose.mjs +98 -52
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NodPay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/SKILL.md
CHANGED
|
@@ -156,7 +156,7 @@ EOA wallets: replace passkey fields with `"humanSignerEoa": "0x..."`.
|
|
|
156
156
|
|
|
157
157
|
| Flag | Required | Description |
|
|
158
158
|
|------|----------|-------------|
|
|
159
|
-
| `--chain` | ✅ | `
|
|
159
|
+
| `--chain` | ✅ | `base`, `arbitrum`, `polygon`, `optimism`, `ethereum`, `sepolia`, `base_sepolia` |
|
|
160
160
|
| `--safe` | ✅ | Wallet address |
|
|
161
161
|
| `--to` | ✅ | Recipient |
|
|
162
162
|
| `--value-eth` | ✅ | Amount in ETH |
|
|
@@ -165,7 +165,7 @@ EOA wallets: replace passkey fields with `"humanSignerEoa": "0x..."`.
|
|
|
165
165
|
| `--recovery-signer` | first tx | Recovery signer address |
|
|
166
166
|
| `--nonce` | **required** | Nonce for this proposal. Run `txs` first to determine. |
|
|
167
167
|
|
|
168
|
-
Wallet address is the same across all chains. **Ask which chain if not specified.**
|
|
168
|
+
Wallet address is the same across all chains. **Ask which chain if not specified.** Prefer L2 chains (Base, Arbitrum, etc.) — Ethereum mainnet gas is significantly more expensive.
|
|
169
169
|
|
|
170
170
|
---
|
|
171
171
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodpay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.36",
|
|
4
4
|
"description": "NodPay CLI — propose on-chain payments from agent-human shared wallets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"postpublish": "curl -s https://purge.jsdelivr.net/npm/nodpay@latest/SKILL.md > /dev/null && echo 'jsdelivr cache purged'"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@nodpay/core": "^0.1.
|
|
34
|
+
"@nodpay/core": "^0.1.3",
|
|
35
35
|
"@safe-global/relay-kit": "^4.1.1",
|
|
36
36
|
"ethers": "^6.16.0"
|
|
37
37
|
}
|
package/scripts/env.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified environment variable loader for NodPay CLI.
|
|
3
|
+
*
|
|
4
|
+
* Priority (highest wins):
|
|
5
|
+
* 1. process.env (real env — set by caller, SafeClaw injection, container, etc.)
|
|
6
|
+
* 2. ~/.nodpay/.env file (convenience layer for local dev)
|
|
7
|
+
*
|
|
8
|
+
* The .env file is loaded into process.env WITHOUT overriding existing values.
|
|
9
|
+
* This matches standard dotenv behavior and Unix conventions:
|
|
10
|
+
* NODPAY_AGENT_KEY=0xabc npx nodpay propose → uses 0xabc, ignores file
|
|
11
|
+
*
|
|
12
|
+
* After loadDotEnv(), all code reads from process.env uniformly.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
|
|
18
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
19
|
+
const DEFAULT_ENV_PATH = join(HOME, '.nodpay', '.env');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse a .env file and load values into process.env.
|
|
23
|
+
* Does NOT override existing env vars (real env takes priority).
|
|
24
|
+
*
|
|
25
|
+
* @param {string} [envPath] - Path to .env file. Defaults to ~/.nodpay/.env
|
|
26
|
+
*/
|
|
27
|
+
export function loadDotEnv(envPath = DEFAULT_ENV_PATH) {
|
|
28
|
+
if (!existsSync(envPath)) return;
|
|
29
|
+
try {
|
|
30
|
+
const lines = readFileSync(envPath, 'utf8').split('\n');
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
34
|
+
const eqIdx = trimmed.indexOf('=');
|
|
35
|
+
const name = trimmed.slice(0, eqIdx).trim();
|
|
36
|
+
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
37
|
+
// Only set if not already in process.env (real env wins)
|
|
38
|
+
if (!(name in process.env)) {
|
|
39
|
+
process.env[name] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Non-fatal: .env file may not exist or be unreadable
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get an env var after .env has been loaded.
|
|
49
|
+
* Convenience wrapper — just reads process.env.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} name - Environment variable name
|
|
52
|
+
* @param {string} [fallback] - Default value if not set
|
|
53
|
+
* @returns {string|undefined}
|
|
54
|
+
*/
|
|
55
|
+
export function env(name, fallback) {
|
|
56
|
+
return process.env[name] || fallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { HOME, DEFAULT_ENV_PATH };
|
package/scripts/keygen.mjs
CHANGED
|
@@ -14,41 +14,32 @@
|
|
|
14
14
|
import { Wallet } from 'ethers';
|
|
15
15
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs';
|
|
16
16
|
import { resolve, dirname, join } from 'path';
|
|
17
|
+
import { loadDotEnv, env, HOME } from './env.mjs';
|
|
17
18
|
|
|
18
19
|
const args = process.argv.slice(2);
|
|
19
20
|
const envFileIdx = args.indexOf('--env-file');
|
|
20
|
-
const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
|
21
21
|
const envFile = envFileIdx !== -1
|
|
22
22
|
? resolve(args[envFileIdx + 1])
|
|
23
23
|
: join(HOME, '.nodpay', '.env');
|
|
24
24
|
|
|
25
25
|
const ENV_VAR = 'NODPAY_AGENT_KEY';
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
if (!existsSync(envFile)) return null;
|
|
30
|
-
const lines = readFileSync(envFile, 'utf8').split('\n');
|
|
31
|
-
for (const line of lines) {
|
|
32
|
-
const trimmed = line.trim();
|
|
33
|
-
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
34
|
-
const [name, ...rest] = trimmed.split('=');
|
|
35
|
-
if (name.trim() === ENV_VAR) {
|
|
36
|
-
const value = rest.join('=').trim().replace(/^["']|["']$/g, '');
|
|
37
|
-
if (value) return value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
27
|
+
// Load .env file into process.env (real env takes priority)
|
|
28
|
+
loadDotEnv(envFile);
|
|
42
29
|
|
|
43
|
-
|
|
30
|
+
// Check if key already exists (process.env — from real env or .env file)
|
|
31
|
+
const existing = env(ENV_VAR);
|
|
44
32
|
|
|
45
33
|
if (existing) {
|
|
46
34
|
try {
|
|
47
35
|
const wallet = new Wallet(existing);
|
|
48
36
|
console.log(wallet.address);
|
|
49
|
-
|
|
37
|
+
const source = process.env[ENV_VAR] === existing && !existsSync(envFile)
|
|
38
|
+
? 'environment variable'
|
|
39
|
+
: envFile;
|
|
40
|
+
console.error(`${ENV_VAR} already configured (${source})`);
|
|
50
41
|
} catch {
|
|
51
|
-
console.error(`${ENV_VAR}
|
|
42
|
+
console.error(`${ENV_VAR} is set but invalid. Check your env or ${envFile}.`);
|
|
52
43
|
process.exit(1);
|
|
53
44
|
}
|
|
54
45
|
} else {
|
package/scripts/propose.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The agent signs first (1 of 2). The serialized SafeOperation is
|
|
7
7
|
* output so the web app can have the user co-sign and submit.
|
|
8
8
|
*
|
|
9
|
-
* Agent key:
|
|
9
|
+
* Agent key: from process.env.NODPAY_AGENT_KEY (real env > ~/.nodpay/.env file > --remote-signer).
|
|
10
10
|
* Chain config: resolved via --chain from @nodpay/core networks registry.
|
|
11
11
|
* Bundler: NodPay public proxy (override with OP_STORE_URL for self-hosted).
|
|
12
12
|
*
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* --safe <address> - Wallet (Safe) address
|
|
18
18
|
* --counterfactual - Safe not yet deployed; include deployment in UserOp
|
|
19
19
|
* --human-signer-eoa <address> - Human's EOA signer address (for EOA mode)
|
|
20
|
+
* --remote-signer <url> - SafeClaw proxy URL for remote signing (mutually exclusive with --human-signer-eoa)
|
|
20
21
|
* --salt <nonce> - Salt nonce (required for counterfactual)
|
|
21
22
|
* --reuse-gas-from <shortHash> - Reuse gas values from a previous op (shortHash prefix of safeOpHash)
|
|
22
23
|
* --nonce <n> - Required. Use `txs` to find current nonce.
|
|
@@ -26,15 +27,22 @@
|
|
|
26
27
|
|
|
27
28
|
import { Safe4337Pack } from '@safe-global/relay-kit';
|
|
28
29
|
import { ethers } from 'ethers';
|
|
29
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
30
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
30
31
|
import { join, dirname } from 'path';
|
|
31
32
|
import { fileURLToPath } from 'url';
|
|
32
33
|
import { computeUserOpHash, ENTRYPOINT } from '@nodpay/core';
|
|
34
|
+
import { loadDotEnv, env, HOME } from './env.mjs';
|
|
33
35
|
|
|
34
36
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
37
|
const PENDING_DIR = join(__dirname, '..', '.pending-txs');
|
|
36
38
|
mkdirSync(PENDING_DIR, { recursive: true });
|
|
37
39
|
|
|
40
|
+
// Load ~/.nodpay/.env into process.env (real env takes priority, file is fallback)
|
|
41
|
+
const _envFileArg = process.argv.includes('--env-file')
|
|
42
|
+
? process.argv[process.argv.indexOf('--env-file') + 1]
|
|
43
|
+
: undefined;
|
|
44
|
+
loadDotEnv(_envFileArg);
|
|
45
|
+
|
|
38
46
|
// Resolve chain config: --chain flag auto-resolves from networks.json, env vars as fallback
|
|
39
47
|
import { createRequire } from 'module';
|
|
40
48
|
const require = createRequire(import.meta.url);
|
|
@@ -63,57 +71,70 @@ if (chainArg) {
|
|
|
63
71
|
}
|
|
64
72
|
const ENTRYPOINT_ADDRESS = ENTRYPOINT;
|
|
65
73
|
|
|
66
|
-
|
|
74
|
+
// SECURITY: Agent key is read from process.env.NODPAY_AGENT_KEY.
|
|
75
|
+
// The .env file was loaded above (loadDotEnv) without overriding real env vars.
|
|
76
|
+
// Priority: real env (SafeClaw injection, container, explicit) > .env file.
|
|
77
|
+
// The key never appears in stdout or agent conversation history.
|
|
67
78
|
|
|
68
|
-
// SECURITY: Read agent key from ~/.nodpay/.env file (chmod 600), not from
|
|
69
|
-
// process.env or CLI args. The key is loaded at runtime by the script itself,
|
|
70
|
-
// so it never passes through the LLM agent's context or conversation history.
|
|
71
|
-
function loadAgentKey() {
|
|
72
|
-
try {
|
|
73
|
-
const envPath = join(HOME, '.nodpay', '.env');
|
|
74
|
-
const lines = readFileSync(envPath, 'utf8').split('\n');
|
|
75
|
-
for (const line of lines) {
|
|
76
|
-
const trimmed = line.trim();
|
|
77
|
-
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
78
|
-
const [name, ...rest] = trimmed.split('=');
|
|
79
|
-
if (name.trim() === 'NODPAY_AGENT_KEY') {
|
|
80
|
-
return rest.join('=').trim().replace(/^["']|["']$/g, '');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
} catch {}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
const NODPAY_AGENT_KEY = loadAgentKey();
|
|
87
79
|
const DEFAULT_SAFE = null; // always use --safe flag
|
|
88
80
|
|
|
89
81
|
// BUNDLER: NodPay provides a public bundler proxy at nodpay.ai/api/bundler so
|
|
90
82
|
// agents don't need their own bundler API key. This is a thin relay — it
|
|
91
83
|
// forwards the UserOp to a bundler service and returns the result. The proxy
|
|
92
84
|
// only sees the already-signed (partial) UserOp; it cannot modify or execute it.
|
|
93
|
-
|
|
94
|
-
function loadDotEnvVar(name, fallback) {
|
|
95
|
-
try {
|
|
96
|
-
const envPath = join(HOME, '.nodpay', '.env');
|
|
97
|
-
const lines = readFileSync(envPath, 'utf8').split('\n');
|
|
98
|
-
for (const line of lines) {
|
|
99
|
-
const trimmed = line.trim();
|
|
100
|
-
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
101
|
-
const [k, ...rest] = trimmed.split('=');
|
|
102
|
-
if (k.trim() === name) return rest.join('=').trim().replace(/^["']|["']$/g, '');
|
|
103
|
-
}
|
|
104
|
-
} catch {}
|
|
105
|
-
return fallback;
|
|
106
|
-
}
|
|
107
|
-
const opStoreBase = loadDotEnvVar('OP_STORE_URL', 'https://nodpay.ai/api');
|
|
85
|
+
const opStoreBase = env('OP_STORE_URL', 'https://nodpay.ai/api');
|
|
108
86
|
const BUNDLER_URL = `${opStoreBase}/bundler/${CHAIN_ID}`;
|
|
109
87
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
process.
|
|
113
|
-
|
|
88
|
+
// --- Agent key: remote signer (SafeClaw proxy) or local key (~/.nodpay/.env) ---
|
|
89
|
+
const _remoteSignerUrl = process.argv.includes('--remote-signer')
|
|
90
|
+
? process.argv[process.argv.indexOf('--remote-signer') + 1]
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
93
|
+
let AGENT_ADDRESS;
|
|
94
|
+
let signHash;
|
|
95
|
+
let _localAgentKey; // only set in local mode, used for SDK init (EOA signer)
|
|
96
|
+
|
|
97
|
+
if (_remoteSignerUrl) {
|
|
98
|
+
// Remote signer mode: get address + signing from SafeClaw proxy
|
|
99
|
+
const addrRes = await fetch(`${_remoteSignerUrl}/address`);
|
|
100
|
+
if (!addrRes.ok) {
|
|
101
|
+
const err = await addrRes.json().catch(() => ({}));
|
|
102
|
+
console.error(JSON.stringify({
|
|
103
|
+
error: err.error || 'Failed to get agent address from remote signer',
|
|
104
|
+
code: err.code || 'REMOTE_SIGNER_ERROR'
|
|
105
|
+
}));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
AGENT_ADDRESS = (await addrRes.json()).address;
|
|
109
|
+
|
|
110
|
+
signHash = async (hash) => {
|
|
111
|
+
const signRes = await fetch(`${_remoteSignerUrl}/sign/${AGENT_ADDRESS}`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
body: JSON.stringify({ hash })
|
|
115
|
+
});
|
|
116
|
+
if (!signRes.ok) {
|
|
117
|
+
const err = await signRes.json().catch(() => ({}));
|
|
118
|
+
throw new Error(err.error || 'Remote signing failed');
|
|
119
|
+
}
|
|
120
|
+
return (await signRes.json()).signature;
|
|
121
|
+
};
|
|
122
|
+
} else {
|
|
123
|
+
// Local mode: read key from process.env (loaded from real env or ~/.nodpay/.env)
|
|
124
|
+
const NODPAY_AGENT_KEY = env('NODPAY_AGENT_KEY');
|
|
125
|
+
if (!NODPAY_AGENT_KEY) {
|
|
126
|
+
console.error(JSON.stringify({ error: 'Missing NODPAY_AGENT_KEY — set env var, add to ~/.nodpay/.env, or use --remote-signer' }));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
_localAgentKey = NODPAY_AGENT_KEY;
|
|
130
|
+
const agentWallet = new ethers.Wallet(NODPAY_AGENT_KEY);
|
|
131
|
+
AGENT_ADDRESS = agentWallet.address;
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
const
|
|
133
|
+
signHash = async (hash) => {
|
|
134
|
+
const sig = agentWallet.signingKey.sign(hash);
|
|
135
|
+
return ethers.Signature.from(sig).serialized;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
117
138
|
|
|
118
139
|
const args = process.argv.slice(2);
|
|
119
140
|
function getArg(name) {
|
|
@@ -139,6 +160,12 @@ const passkeyVerifier = getArg('--passkey-verifier') || '0x445a0683e494ea0c5AF3E
|
|
|
139
160
|
const recoverySigner = getArg('--recovery-signer');
|
|
140
161
|
const isPasskey = !!(passkeyX && passkeyY);
|
|
141
162
|
|
|
163
|
+
// Phase 1: --remote-signer + --human-signer-eoa is not supported
|
|
164
|
+
if (_remoteSignerUrl && humanSigner) {
|
|
165
|
+
console.error(JSON.stringify({ error: '--remote-signer and --human-signer-eoa cannot be combined. Remote signer mode only supports passkey wallets.' }));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
|
|
142
169
|
if (!to) {
|
|
143
170
|
console.error(JSON.stringify({ error: 'Missing --to <address>' }));
|
|
144
171
|
process.exit(1);
|
|
@@ -151,6 +178,18 @@ if (!ethers.isAddress(to)) {
|
|
|
151
178
|
|
|
152
179
|
const SAFE_ADDRESS = safeOverride || DEFAULT_SAFE;
|
|
153
180
|
|
|
181
|
+
// Read optional wallet JSON for rpId (SafeClaw cross-origin passkey support)
|
|
182
|
+
let walletRpId = null;
|
|
183
|
+
if (SAFE_ADDRESS) {
|
|
184
|
+
const walletJsonPath = join(HOME, '.nodpay', 'wallets', `${SAFE_ADDRESS}.json`);
|
|
185
|
+
if (existsSync(walletJsonPath)) {
|
|
186
|
+
try {
|
|
187
|
+
const walletData = JSON.parse(readFileSync(walletJsonPath, 'utf8'));
|
|
188
|
+
if (walletData.rpId) walletRpId = walletData.rpId;
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
154
193
|
if (!isCounterfactual && !SAFE_ADDRESS) {
|
|
155
194
|
console.error(JSON.stringify({ error: 'Missing SAFE_ADDRESS. Use --safe <address> or set SAFE_ADDRESS env, or use --counterfactual.' }));
|
|
156
195
|
process.exit(1);
|
|
@@ -290,8 +329,8 @@ try {
|
|
|
290
329
|
initOptions.options = { safeAddress: SAFE_ADDRESS };
|
|
291
330
|
}
|
|
292
331
|
} else {
|
|
293
|
-
// EOA signer: agent key as primary signer
|
|
294
|
-
initOptions.signer =
|
|
332
|
+
// EOA signer: agent key as primary signer (requires local key)
|
|
333
|
+
initOptions.signer = _localAgentKey;
|
|
295
334
|
if (isCounterfactual) {
|
|
296
335
|
// Canonical owner order: [humanSigner, agentSigner, recoverySigner] — must match frontend
|
|
297
336
|
const eoaOwners = recoverySigner
|
|
@@ -448,11 +487,8 @@ try {
|
|
|
448
487
|
verifyingContract: safeOperation.options.moduleAddress,
|
|
449
488
|
};
|
|
450
489
|
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
// a different hash than getHash(), making server-side verification impossible)
|
|
454
|
-
const sig = agentWallet.signingKey.sign(safeOpHash);
|
|
455
|
-
const agentSig = ethers.Signature.from(sig).serialized;
|
|
490
|
+
// Sign with agent key: local wallet or remote signer (SafeClaw proxy)
|
|
491
|
+
const agentSig = await signHash(safeOpHash);
|
|
456
492
|
safeOperation.addSignature({
|
|
457
493
|
signer: AGENT_ADDRESS,
|
|
458
494
|
data: agentSig,
|
|
@@ -515,6 +551,10 @@ try {
|
|
|
515
551
|
agent: AGENT_ADDRESS,
|
|
516
552
|
agentSignature: safeOperationJson.signatures,
|
|
517
553
|
createdAt: new Date().toISOString(),
|
|
554
|
+
// SafeClaw integration: passkey coordinates + recovery for approve page fallback
|
|
555
|
+
passkeyX: passkeyX || null,
|
|
556
|
+
passkeyY: passkeyY || null,
|
|
557
|
+
recoveryAddress: recoverySigner || null,
|
|
518
558
|
};
|
|
519
559
|
|
|
520
560
|
// Extract raw agent signature for server auth
|
|
@@ -537,8 +577,14 @@ try {
|
|
|
537
577
|
result.opStoreError = storeData.error || `HTTP ${storeRes.status}`;
|
|
538
578
|
}
|
|
539
579
|
if (storeData.shortHash) {
|
|
540
|
-
const webBase =
|
|
541
|
-
|
|
580
|
+
const webBase = env('WEB_APP_URL', 'https://nodpay.ai/');
|
|
581
|
+
// Build approve URL with passkey params for SafeClaw users (no localStorage)
|
|
582
|
+
const approveParams = new URLSearchParams({ safeOpHash: storeData.safeOpHash });
|
|
583
|
+
if (passkeyX) approveParams.set('px', passkeyX);
|
|
584
|
+
if (passkeyY) approveParams.set('py', passkeyY);
|
|
585
|
+
if (recoverySigner) approveParams.set('recovery', recoverySigner);
|
|
586
|
+
if (walletRpId) approveParams.set('rpId', walletRpId);
|
|
587
|
+
approveUrl = `${webBase}approve?${approveParams.toString()}`;
|
|
542
588
|
result.approveUrl = approveUrl;
|
|
543
589
|
result.opStoreSafeOpHash = storeData.safeOpHash;
|
|
544
590
|
result.opStoreShortHash = storeData.shortHash;
|