@crediolabs/policy-synth 0.1.13 → 0.1.14
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/dist/install/build-install-policy.js +81 -50
- package/dist/install/oz-auth.d.ts +27 -0
- package/dist/install/oz-auth.js +105 -0
- package/dist-cjs/install/build-install-policy.js +81 -50
- package/dist-cjs/install/oz-auth.d.ts +27 -0
- package/dist-cjs/install/oz-auth.js +114 -0
- package/package.json +1 -1
- package/src/install/build-install-policy.ts +139 -72
- package/src/install/oz-auth.ts +140 -0
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { createHash } from 'node:crypto';
|
|
20
20
|
import { Address, BASE_FEE, Contract, Keypair, Operation, rpc, scValToBigInt, TransactionBuilder, xdr, } from '@stellar/stellar-sdk';
|
|
21
21
|
import { buildAddContextRuleArgs, DEFAULT_GRAMMAR_VERSION } from "./build-add-context-rule.js";
|
|
22
|
+
import { accountEntry, authDigest, authPayload, delegatedSignerEntry, signaturePayload, } from "./oz-auth.js";
|
|
22
23
|
/** Convert a real rpc.Server into the InstallRpcClient surface. The
|
|
23
24
|
* `getContractVersion` lookup uses `simulateTransaction` against
|
|
24
25
|
* `contract.call('grammar_version')` and decodes the returned u32.
|
|
@@ -83,31 +84,28 @@ export async function buildInstallPolicyXdr(args) {
|
|
|
83
84
|
...(args.oracleParams ? { oracleParams: args.oracleParams } : {}),
|
|
84
85
|
...(args.grammarVersion !== undefined ? { grammarVersion: args.grammarVersion } : {}),
|
|
85
86
|
});
|
|
86
|
-
// 2.
|
|
87
|
+
// 2. Fetch the source sequence, then build the recording transaction with a
|
|
88
|
+
// bare host call. The recording pass assigns the smart-account auth nonce
|
|
89
|
+
// and root invocation needed to construct OZ's AuthPayload.
|
|
87
90
|
const source = await args.rpc.getAccount(args.sourceAccount);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
functionName: 'add_context_rule',
|
|
95
|
-
args: [...callArgs],
|
|
96
|
-
})),
|
|
97
|
-
auth: [],
|
|
98
|
-
});
|
|
91
|
+
const hostFunction = xdr.HostFunction.hostFunctionTypeInvokeContract(new xdr.InvokeContractArgs({
|
|
92
|
+
contractAddress: new Address(args.smartAccount).toScAddress(),
|
|
93
|
+
functionName: 'add_context_rule',
|
|
94
|
+
args: [...callArgs],
|
|
95
|
+
}));
|
|
96
|
+
const makeOperation = (auth = []) => Operation.invokeHostFunction({ func: hostFunction, auth });
|
|
99
97
|
const baseFee = args.baseFee !== undefined ? String(args.baseFee) : BASE_FEE;
|
|
100
|
-
const
|
|
98
|
+
const buildTx = (op) => buildUnsignedTx({
|
|
101
99
|
sourceAccount: args.sourceAccount,
|
|
102
100
|
sequence: source.sequenceNumber(),
|
|
103
101
|
fee: baseFee,
|
|
104
102
|
networkPassphrase: args.networkPassphrase,
|
|
105
103
|
op,
|
|
106
104
|
});
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
const recorded = await args.rpc.simulateTransaction(
|
|
105
|
+
const recordingTx = buildTx(makeOperation());
|
|
106
|
+
// 3. Recording simulation: capture the smart account's nonce and invocation
|
|
107
|
+
// tree. Nothing is signed here.
|
|
108
|
+
const recorded = await args.rpc.simulateTransaction(recordingTx);
|
|
111
109
|
if (rpc.Api.isSimulationError(recorded)) {
|
|
112
110
|
// Short, stable reason. The full `simulateTransaction` error (which
|
|
113
111
|
// carries host + URL detail) stays in the SDK's own logs - never
|
|
@@ -121,14 +119,32 @@ export async function buildInstallPolicyXdr(args) {
|
|
|
121
119
|
if (!original) {
|
|
122
120
|
throw new Error(`install_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`);
|
|
123
121
|
}
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
// 4. `add_context_rule` is authorised by the deploy-time admin rule (rule 0).
|
|
123
|
+
// The delegated signer uses source-account credentials, so its signature
|
|
124
|
+
// bytes stay empty and the ordinary transaction-envelope signature covers
|
|
125
|
+
// the call when the consumer signs it.
|
|
126
|
+
const validUntilLedger = (await args.rpc.getLatestLedger()).sequence +
|
|
127
|
+
(args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS);
|
|
128
|
+
const contextRuleIds = [0];
|
|
129
|
+
const digest = authDigest(signaturePayload(args.networkPassphrase, original.credentials().address().nonce(), validUntilLedger, original.rootInvocation()), contextRuleIds);
|
|
130
|
+
const authEntries = [
|
|
131
|
+
accountEntry(original, validUntilLedger, authPayload([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))),
|
|
132
|
+
...contextRuleIds.map(() => delegatedSignerEntry(args.smartAccount, digest)),
|
|
133
|
+
];
|
|
134
|
+
// 5. Simulate again with the OZ entries already attached. The SDK preserves
|
|
135
|
+
// existing auth during assembly and adds the simulated Soroban footprint +
|
|
136
|
+
// resource fee, yielding a complete unsigned envelope.
|
|
137
|
+
const txWithAuth = buildTx(makeOperation(authEntries));
|
|
138
|
+
const enforcing = await args.rpc.simulateTransaction(txWithAuth);
|
|
139
|
+
if (rpc.Api.isSimulationError(enforcing)) {
|
|
140
|
+
throw new Error('install_policy: auth simulateTransaction failed');
|
|
141
|
+
}
|
|
142
|
+
const finalTx = rpc.assembleTransaction(txWithAuth, enforcing).build();
|
|
143
|
+
// 6. Decode the structured description FROM the final assembled transaction.
|
|
144
|
+
// The human approval binds to the exact bytes the wallet will sign.
|
|
145
|
+
const describes = decodeInstallCallDescribes(finalTx, args.installNonce);
|
|
130
146
|
return {
|
|
131
|
-
unsignedXdr:
|
|
147
|
+
unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
|
|
132
148
|
smartAccount: args.smartAccount,
|
|
133
149
|
sourceAccount: args.sourceAccount,
|
|
134
150
|
call: { contract: args.smartAccount, fn: 'add_context_rule' },
|
|
@@ -157,25 +173,21 @@ export async function buildInstallPolicyXdr(args) {
|
|
|
157
173
|
* be guessing at a contract it cannot read, and the chain is the authority. */
|
|
158
174
|
export async function buildRevokePolicyXdr(args) {
|
|
159
175
|
const source = await args.rpc.getAccount(args.sourceAccount);
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
args: [xdr.ScVal.scvU32(args.ruleId)],
|
|
167
|
-
})),
|
|
168
|
-
auth: [],
|
|
169
|
-
});
|
|
176
|
+
const hostFunction = xdr.HostFunction.hostFunctionTypeInvokeContract(new xdr.InvokeContractArgs({
|
|
177
|
+
contractAddress: new Address(args.smartAccount).toScAddress(),
|
|
178
|
+
functionName: 'remove_context_rule',
|
|
179
|
+
args: [xdr.ScVal.scvU32(args.ruleId)],
|
|
180
|
+
}));
|
|
181
|
+
const makeOperation = (auth = []) => Operation.invokeHostFunction({ func: hostFunction, auth });
|
|
170
182
|
const baseFee = args.baseFee !== undefined ? String(args.baseFee) : BASE_FEE;
|
|
171
|
-
const
|
|
183
|
+
const buildTx = (op) => buildUnsignedTx({
|
|
172
184
|
sourceAccount: args.sourceAccount,
|
|
173
185
|
sequence: source.sequenceNumber(),
|
|
174
186
|
fee: baseFee,
|
|
175
187
|
networkPassphrase: args.networkPassphrase,
|
|
176
188
|
op,
|
|
177
189
|
});
|
|
178
|
-
const recorded = await args.rpc.simulateTransaction(
|
|
190
|
+
const recorded = await args.rpc.simulateTransaction(buildTx(makeOperation()));
|
|
179
191
|
if (rpc.Api.isSimulationError(recorded)) {
|
|
180
192
|
// Short, stable reason. The full `simulateTransaction` error (which
|
|
181
193
|
// carries host + URL detail) stays in the SDK's own logs - never
|
|
@@ -189,8 +201,27 @@ export async function buildRevokePolicyXdr(args) {
|
|
|
189
201
|
if (!original) {
|
|
190
202
|
throw new Error(`revoke_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`);
|
|
191
203
|
}
|
|
204
|
+
// Removal is master-only, so the deploy-time admin rule authorises it. The
|
|
205
|
+
// recorded rootInvocation still includes remove_context_rule(ruleId), binding
|
|
206
|
+
// the auth payload to the exact rule being removed. As with install,
|
|
207
|
+
// source-account credentials carry the delegated signer entry and the
|
|
208
|
+
// consumer supplies only the ordinary envelope signature.
|
|
209
|
+
const validUntilLedger = (await args.rpc.getLatestLedger()).sequence +
|
|
210
|
+
(args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS);
|
|
211
|
+
const contextRuleIds = [0];
|
|
212
|
+
const digest = authDigest(signaturePayload(args.networkPassphrase, original.credentials().address().nonce(), validUntilLedger, original.rootInvocation()), contextRuleIds);
|
|
213
|
+
const authEntries = [
|
|
214
|
+
accountEntry(original, validUntilLedger, authPayload([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))),
|
|
215
|
+
...contextRuleIds.map(() => delegatedSignerEntry(args.smartAccount, digest)),
|
|
216
|
+
];
|
|
217
|
+
const txWithAuth = buildTx(makeOperation(authEntries));
|
|
218
|
+
const enforcing = await args.rpc.simulateTransaction(txWithAuth);
|
|
219
|
+
if (rpc.Api.isSimulationError(enforcing)) {
|
|
220
|
+
throw new Error('revoke_policy: auth simulateTransaction failed');
|
|
221
|
+
}
|
|
222
|
+
const finalTx = rpc.assembleTransaction(txWithAuth, enforcing).build();
|
|
192
223
|
return {
|
|
193
|
-
unsignedXdr:
|
|
224
|
+
unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
|
|
194
225
|
smartAccount: args.smartAccount,
|
|
195
226
|
sourceAccount: args.sourceAccount,
|
|
196
227
|
call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
|
|
@@ -200,7 +231,7 @@ export async function buildRevokePolicyXdr(args) {
|
|
|
200
231
|
};
|
|
201
232
|
}
|
|
202
233
|
/** ~25 minutes at 5s/ledger. */
|
|
203
|
-
const
|
|
234
|
+
const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300;
|
|
204
235
|
// ---- internals ----
|
|
205
236
|
/** Adapter: turn the install-policy wire `PolicyRef` shape into the core
|
|
206
237
|
* `PolicyRef` shape `buildAddContextRuleArgs` expects. Keeps the wire
|
|
@@ -238,20 +269,24 @@ function buildUnsignedTx(args) {
|
|
|
238
269
|
// its signature, and broadcasts. We do NOT sign here.
|
|
239
270
|
return tx;
|
|
240
271
|
}
|
|
241
|
-
/** Decode the install call's structured fields directly out of the
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* will
|
|
245
|
-
*
|
|
246
|
-
* a ToolError, since that would mean the XDR was tampered with (or
|
|
272
|
+
/** Decode the install call's structured fields directly out of the final
|
|
273
|
+
* assembled transaction. The bytes are the source of truth: a human reviewing
|
|
274
|
+
* the install wants to see what the wallet will actually sign, not what we
|
|
275
|
+
* think it will. The decode is deliberately strict - any shape we did not
|
|
276
|
+
* build ourselves throws, since that would mean the XDR was tampered with (or
|
|
247
277
|
* the encoder changed and this descriptor lags).
|
|
248
278
|
*
|
|
249
279
|
* `expectedInstallNonce` is a fallback for the rare case where the
|
|
250
280
|
* install has no interpreter policy (no `install_nonce` to read); the
|
|
251
281
|
* caller already knows it. */
|
|
252
|
-
function decodeInstallCallDescribes(tx,
|
|
282
|
+
function decodeInstallCallDescribes(tx, expectedInstallNonce) {
|
|
283
|
+
const operations = tx.toEnvelope().v1().tx().operations();
|
|
284
|
+
if (operations.length !== 1 || !operations[0]) {
|
|
285
|
+
throw new Error(`install_policy: final transaction has ${operations.length} operations, expected 1`);
|
|
286
|
+
}
|
|
287
|
+
const op = operations[0];
|
|
253
288
|
const hostFn = op.body().invokeHostFunctionOp()?.hostFunction();
|
|
254
|
-
if (
|
|
289
|
+
if (hostFn?.switch().name !== 'hostFunctionTypeInvokeContract') {
|
|
255
290
|
throw new Error('install_policy: built op is not an invokeHostFunction(invokeContract(...)) call');
|
|
256
291
|
}
|
|
257
292
|
const invokeArgs = hostFn.invokeContract();
|
|
@@ -404,10 +439,6 @@ function decodeInstallCallDescribes(tx, op, expectedInstallNonce) {
|
|
|
404
439
|
// `observedInstallNonce` is the nonce baked into whichever interpreter
|
|
405
440
|
// policy is present; when none, fall back to the caller-supplied value.
|
|
406
441
|
const installNonce = observedInstallNonce ?? expectedInstallNonce;
|
|
407
|
-
// Touch `tx` so the linter does not flag it as unused - the parameter
|
|
408
|
-
// documents that the caller built this Transaction; we re-parse the
|
|
409
|
-
// operation off it as a sanity check.
|
|
410
|
-
void tx;
|
|
411
442
|
return {
|
|
412
443
|
targetContract,
|
|
413
444
|
fnName: 'add_context_rule',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { xdr } from '@stellar/stellar-sdk';
|
|
2
|
+
/** `Signer::Delegated(addr)`. */
|
|
3
|
+
export declare const delegatedSigner: (a: string) => xdr.ScVal;
|
|
4
|
+
/** The standard Soroban authorization preimage hash for one entry. */
|
|
5
|
+
export declare function signaturePayload(networkPassphrase: string, nonce: xdr.Int64, signatureExpirationLedger: number, invocation: xdr.SorobanAuthorizedInvocation): Buffer;
|
|
6
|
+
/**
|
|
7
|
+
* `sha256(signature_payload || xdr(context_rule_ids))`.
|
|
8
|
+
*
|
|
9
|
+
* OZ binds the selected rule ids into the digest so a signature for one rule
|
|
10
|
+
* cannot be replayed against another.
|
|
11
|
+
*/
|
|
12
|
+
export declare function authDigest(payload: Buffer, contextRuleIds: number[]): Buffer;
|
|
13
|
+
/** `AuthPayload { signers, context_rule_ids }` - the account's "signature". */
|
|
14
|
+
export declare function authPayload(signerAddresses: string[], contextRuleIds: number[], signatureFor: (addr: string) => Buffer): xdr.ScVal;
|
|
15
|
+
/**
|
|
16
|
+
* The nested entry a `Delegated` signer needs.
|
|
17
|
+
*
|
|
18
|
+
* `require_auth_for_args` authorizes the CURRENT frame, which while
|
|
19
|
+
* `__check_auth` is running is the account contract executing `__check_auth`,
|
|
20
|
+
* with the digest as its single argument.
|
|
21
|
+
*
|
|
22
|
+
* The signer is the transaction source here, so source-account credentials
|
|
23
|
+
* carry it and no separate signature is required.
|
|
24
|
+
*/
|
|
25
|
+
export declare function delegatedSignerEntry(accountId: string, digest: Buffer): xdr.SorobanAuthorizationEntry;
|
|
26
|
+
/** Rebuild the account's entry with the AuthPayload in the signature slot. */
|
|
27
|
+
export declare function accountEntry(original: xdr.SorobanAuthorizationEntry, signatureExpirationLedger: number, payload: xdr.ScVal): xdr.SorobanAuthorizationEntry;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Build the authorization entries OpenZeppelin's smart account requires.
|
|
2
|
+
//
|
|
3
|
+
// This is the piece that makes an end-to-end test of the account -> policy
|
|
4
|
+
// path possible. Without it, every attempt to exercise `__check_auth`
|
|
5
|
+
// produces a false positive: mocked auth skips `__check_auth` entirely,
|
|
6
|
+
// direct invocation is rejected by the host, and recording-mode simulation
|
|
7
|
+
// does not verify auth at all.
|
|
8
|
+
//
|
|
9
|
+
// Two entries are needed per call:
|
|
10
|
+
//
|
|
11
|
+
// 1. The ACCOUNT's entry. Its `signature` slot is not a signature - OZ puts
|
|
12
|
+
// an `AuthPayload { signers, context_rule_ids }` there, which
|
|
13
|
+
// `__check_auth` receives as its `signatures` argument.
|
|
14
|
+
//
|
|
15
|
+
// 2. One entry per DELEGATED signer. `do_check_auth` calls
|
|
16
|
+
// `addr.require_auth_for_args((auth_digest,))` for each, which is a
|
|
17
|
+
// nested authorization requirement the host will not record during
|
|
18
|
+
// simulation (it never runs `__check_auth` in recording mode), so it has
|
|
19
|
+
// to be constructed by hand.
|
|
20
|
+
//
|
|
21
|
+
// The digest OZ binds is NOT the raw auth payload hash:
|
|
22
|
+
//
|
|
23
|
+
// auth_digest = sha256(signature_payload || xdr(context_rule_ids))
|
|
24
|
+
//
|
|
25
|
+
// where `signature_payload` is the standard Soroban authorization preimage
|
|
26
|
+
// hash. See `do_check_auth` in
|
|
27
|
+
// stellar-contracts/packages/accounts/src/smart_account/storage.rs.
|
|
28
|
+
import { Address, hash, xdr } from '@stellar/stellar-sdk';
|
|
29
|
+
const sym = (s) => xdr.ScVal.scvSymbol(s);
|
|
30
|
+
const u32 = (n) => xdr.ScVal.scvU32(n);
|
|
31
|
+
const vec = (i) => xdr.ScVal.scvVec(i);
|
|
32
|
+
const bytes = (b) => xdr.ScVal.scvBytes(b);
|
|
33
|
+
function struct(fields) {
|
|
34
|
+
return xdr.ScVal.scvMap(Object.keys(fields)
|
|
35
|
+
.sort()
|
|
36
|
+
.map((k) => new xdr.ScMapEntry({ key: sym(k), val: fields[k] })));
|
|
37
|
+
}
|
|
38
|
+
/** `Signer::Delegated(addr)`. */
|
|
39
|
+
export const delegatedSigner = (a) => vec([sym('Delegated'), new Address(a).toScVal()]);
|
|
40
|
+
/** The standard Soroban authorization preimage hash for one entry. */
|
|
41
|
+
export function signaturePayload(networkPassphrase, nonce, signatureExpirationLedger, invocation) {
|
|
42
|
+
const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(new xdr.HashIdPreimageSorobanAuthorization({
|
|
43
|
+
networkId: hash(Buffer.from(networkPassphrase)),
|
|
44
|
+
nonce,
|
|
45
|
+
signatureExpirationLedger,
|
|
46
|
+
invocation,
|
|
47
|
+
}));
|
|
48
|
+
return hash(preimage.toXDR());
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `sha256(signature_payload || xdr(context_rule_ids))`.
|
|
52
|
+
*
|
|
53
|
+
* OZ binds the selected rule ids into the digest so a signature for one rule
|
|
54
|
+
* cannot be replayed against another.
|
|
55
|
+
*/
|
|
56
|
+
export function authDigest(payload, contextRuleIds) {
|
|
57
|
+
const idsXdr = vec(contextRuleIds.map(u32)).toXDR();
|
|
58
|
+
return hash(Buffer.concat([payload, idsXdr]));
|
|
59
|
+
}
|
|
60
|
+
/** `AuthPayload { signers, context_rule_ids }` - the account's "signature". */
|
|
61
|
+
export function authPayload(signerAddresses, contextRuleIds, signatureFor) {
|
|
62
|
+
return struct({
|
|
63
|
+
signers: xdr.ScVal.scvMap(signerAddresses
|
|
64
|
+
.map((a) => new xdr.ScMapEntry({ key: delegatedSigner(a), val: bytes(signatureFor(a)) }))
|
|
65
|
+
// Host maps must be sorted by key.
|
|
66
|
+
.sort((x, y) => Buffer.compare(x.key().toXDR(), y.key().toXDR()))),
|
|
67
|
+
context_rule_ids: vec(contextRuleIds.map(u32)),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The nested entry a `Delegated` signer needs.
|
|
72
|
+
*
|
|
73
|
+
* `require_auth_for_args` authorizes the CURRENT frame, which while
|
|
74
|
+
* `__check_auth` is running is the account contract executing `__check_auth`,
|
|
75
|
+
* with the digest as its single argument.
|
|
76
|
+
*
|
|
77
|
+
* The signer is the transaction source here, so source-account credentials
|
|
78
|
+
* carry it and no separate signature is required.
|
|
79
|
+
*/
|
|
80
|
+
export function delegatedSignerEntry(accountId, digest) {
|
|
81
|
+
return new xdr.SorobanAuthorizationEntry({
|
|
82
|
+
credentials: xdr.SorobanCredentials.sorobanCredentialsSourceAccount(),
|
|
83
|
+
rootInvocation: new xdr.SorobanAuthorizedInvocation({
|
|
84
|
+
function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(new xdr.InvokeContractArgs({
|
|
85
|
+
contractAddress: new Address(accountId).toScAddress(),
|
|
86
|
+
functionName: '__check_auth',
|
|
87
|
+
args: [bytes(digest)],
|
|
88
|
+
})),
|
|
89
|
+
subInvocations: [],
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Rebuild the account's entry with the AuthPayload in the signature slot. */
|
|
94
|
+
export function accountEntry(original, signatureExpirationLedger, payload) {
|
|
95
|
+
const creds = original.credentials().address();
|
|
96
|
+
return new xdr.SorobanAuthorizationEntry({
|
|
97
|
+
credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(new xdr.SorobanAddressCredentials({
|
|
98
|
+
address: creds.address(),
|
|
99
|
+
nonce: creds.nonce(),
|
|
100
|
+
signatureExpirationLedger,
|
|
101
|
+
signature: payload,
|
|
102
|
+
})),
|
|
103
|
+
rootInvocation: original.rootInvocation(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -27,6 +27,7 @@ const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
|
27
27
|
Object.defineProperty(exports, "Contract", { enumerable: true, get: function () { return stellar_sdk_1.Contract; } });
|
|
28
28
|
const build_add_context_rule_ts_1 = require("./build-add-context-rule.js");
|
|
29
29
|
Object.defineProperty(exports, "DEFAULT_GRAMMAR_VERSION", { enumerable: true, get: function () { return build_add_context_rule_ts_1.DEFAULT_GRAMMAR_VERSION; } });
|
|
30
|
+
const oz_auth_ts_1 = require("./oz-auth.js");
|
|
30
31
|
/** Convert a real rpc.Server into the InstallRpcClient surface. The
|
|
31
32
|
* `getContractVersion` lookup uses `simulateTransaction` against
|
|
32
33
|
* `contract.call('grammar_version')` and decodes the returned u32.
|
|
@@ -91,31 +92,28 @@ async function buildInstallPolicyXdr(args) {
|
|
|
91
92
|
...(args.oracleParams ? { oracleParams: args.oracleParams } : {}),
|
|
92
93
|
...(args.grammarVersion !== undefined ? { grammarVersion: args.grammarVersion } : {}),
|
|
93
94
|
});
|
|
94
|
-
// 2.
|
|
95
|
+
// 2. Fetch the source sequence, then build the recording transaction with a
|
|
96
|
+
// bare host call. The recording pass assigns the smart-account auth nonce
|
|
97
|
+
// and root invocation needed to construct OZ's AuthPayload.
|
|
95
98
|
const source = await args.rpc.getAccount(args.sourceAccount);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
functionName: 'add_context_rule',
|
|
103
|
-
args: [...callArgs],
|
|
104
|
-
})),
|
|
105
|
-
auth: [],
|
|
106
|
-
});
|
|
99
|
+
const hostFunction = stellar_sdk_1.xdr.HostFunction.hostFunctionTypeInvokeContract(new stellar_sdk_1.xdr.InvokeContractArgs({
|
|
100
|
+
contractAddress: new stellar_sdk_1.Address(args.smartAccount).toScAddress(),
|
|
101
|
+
functionName: 'add_context_rule',
|
|
102
|
+
args: [...callArgs],
|
|
103
|
+
}));
|
|
104
|
+
const makeOperation = (auth = []) => stellar_sdk_1.Operation.invokeHostFunction({ func: hostFunction, auth });
|
|
107
105
|
const baseFee = args.baseFee !== undefined ? String(args.baseFee) : stellar_sdk_1.BASE_FEE;
|
|
108
|
-
const
|
|
106
|
+
const buildTx = (op) => buildUnsignedTx({
|
|
109
107
|
sourceAccount: args.sourceAccount,
|
|
110
108
|
sequence: source.sequenceNumber(),
|
|
111
109
|
fee: baseFee,
|
|
112
110
|
networkPassphrase: args.networkPassphrase,
|
|
113
111
|
op,
|
|
114
112
|
});
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
const recorded = await args.rpc.simulateTransaction(
|
|
113
|
+
const recordingTx = buildTx(makeOperation());
|
|
114
|
+
// 3. Recording simulation: capture the smart account's nonce and invocation
|
|
115
|
+
// tree. Nothing is signed here.
|
|
116
|
+
const recorded = await args.rpc.simulateTransaction(recordingTx);
|
|
119
117
|
if (stellar_sdk_1.rpc.Api.isSimulationError(recorded)) {
|
|
120
118
|
// Short, stable reason. The full `simulateTransaction` error (which
|
|
121
119
|
// carries host + URL detail) stays in the SDK's own logs - never
|
|
@@ -129,14 +127,32 @@ async function buildInstallPolicyXdr(args) {
|
|
|
129
127
|
if (!original) {
|
|
130
128
|
throw new Error(`install_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`);
|
|
131
129
|
}
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
// 4. `add_context_rule` is authorised by the deploy-time admin rule (rule 0).
|
|
131
|
+
// The delegated signer uses source-account credentials, so its signature
|
|
132
|
+
// bytes stay empty and the ordinary transaction-envelope signature covers
|
|
133
|
+
// the call when the consumer signs it.
|
|
134
|
+
const validUntilLedger = (await args.rpc.getLatestLedger()).sequence +
|
|
135
|
+
(args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS);
|
|
136
|
+
const contextRuleIds = [0];
|
|
137
|
+
const digest = (0, oz_auth_ts_1.authDigest)((0, oz_auth_ts_1.signaturePayload)(args.networkPassphrase, original.credentials().address().nonce(), validUntilLedger, original.rootInvocation()), contextRuleIds);
|
|
138
|
+
const authEntries = [
|
|
139
|
+
(0, oz_auth_ts_1.accountEntry)(original, validUntilLedger, (0, oz_auth_ts_1.authPayload)([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))),
|
|
140
|
+
...contextRuleIds.map(() => (0, oz_auth_ts_1.delegatedSignerEntry)(args.smartAccount, digest)),
|
|
141
|
+
];
|
|
142
|
+
// 5. Simulate again with the OZ entries already attached. The SDK preserves
|
|
143
|
+
// existing auth during assembly and adds the simulated Soroban footprint +
|
|
144
|
+
// resource fee, yielding a complete unsigned envelope.
|
|
145
|
+
const txWithAuth = buildTx(makeOperation(authEntries));
|
|
146
|
+
const enforcing = await args.rpc.simulateTransaction(txWithAuth);
|
|
147
|
+
if (stellar_sdk_1.rpc.Api.isSimulationError(enforcing)) {
|
|
148
|
+
throw new Error('install_policy: auth simulateTransaction failed');
|
|
149
|
+
}
|
|
150
|
+
const finalTx = stellar_sdk_1.rpc.assembleTransaction(txWithAuth, enforcing).build();
|
|
151
|
+
// 6. Decode the structured description FROM the final assembled transaction.
|
|
152
|
+
// The human approval binds to the exact bytes the wallet will sign.
|
|
153
|
+
const describes = decodeInstallCallDescribes(finalTx, args.installNonce);
|
|
138
154
|
return {
|
|
139
|
-
unsignedXdr:
|
|
155
|
+
unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
|
|
140
156
|
smartAccount: args.smartAccount,
|
|
141
157
|
sourceAccount: args.sourceAccount,
|
|
142
158
|
call: { contract: args.smartAccount, fn: 'add_context_rule' },
|
|
@@ -165,25 +181,21 @@ async function buildInstallPolicyXdr(args) {
|
|
|
165
181
|
* be guessing at a contract it cannot read, and the chain is the authority. */
|
|
166
182
|
async function buildRevokePolicyXdr(args) {
|
|
167
183
|
const source = await args.rpc.getAccount(args.sourceAccount);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
args: [stellar_sdk_1.xdr.ScVal.scvU32(args.ruleId)],
|
|
175
|
-
})),
|
|
176
|
-
auth: [],
|
|
177
|
-
});
|
|
184
|
+
const hostFunction = stellar_sdk_1.xdr.HostFunction.hostFunctionTypeInvokeContract(new stellar_sdk_1.xdr.InvokeContractArgs({
|
|
185
|
+
contractAddress: new stellar_sdk_1.Address(args.smartAccount).toScAddress(),
|
|
186
|
+
functionName: 'remove_context_rule',
|
|
187
|
+
args: [stellar_sdk_1.xdr.ScVal.scvU32(args.ruleId)],
|
|
188
|
+
}));
|
|
189
|
+
const makeOperation = (auth = []) => stellar_sdk_1.Operation.invokeHostFunction({ func: hostFunction, auth });
|
|
178
190
|
const baseFee = args.baseFee !== undefined ? String(args.baseFee) : stellar_sdk_1.BASE_FEE;
|
|
179
|
-
const
|
|
191
|
+
const buildTx = (op) => buildUnsignedTx({
|
|
180
192
|
sourceAccount: args.sourceAccount,
|
|
181
193
|
sequence: source.sequenceNumber(),
|
|
182
194
|
fee: baseFee,
|
|
183
195
|
networkPassphrase: args.networkPassphrase,
|
|
184
196
|
op,
|
|
185
197
|
});
|
|
186
|
-
const recorded = await args.rpc.simulateTransaction(
|
|
198
|
+
const recorded = await args.rpc.simulateTransaction(buildTx(makeOperation()));
|
|
187
199
|
if (stellar_sdk_1.rpc.Api.isSimulationError(recorded)) {
|
|
188
200
|
// Short, stable reason. The full `simulateTransaction` error (which
|
|
189
201
|
// carries host + URL detail) stays in the SDK's own logs - never
|
|
@@ -197,8 +209,27 @@ async function buildRevokePolicyXdr(args) {
|
|
|
197
209
|
if (!original) {
|
|
198
210
|
throw new Error(`revoke_policy: no Soroban auth entry for smart account ${args.smartAccount}; this call does not route through the smart account`);
|
|
199
211
|
}
|
|
212
|
+
// Removal is master-only, so the deploy-time admin rule authorises it. The
|
|
213
|
+
// recorded rootInvocation still includes remove_context_rule(ruleId), binding
|
|
214
|
+
// the auth payload to the exact rule being removed. As with install,
|
|
215
|
+
// source-account credentials carry the delegated signer entry and the
|
|
216
|
+
// consumer supplies only the ordinary envelope signature.
|
|
217
|
+
const validUntilLedger = (await args.rpc.getLatestLedger()).sequence +
|
|
218
|
+
(args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS);
|
|
219
|
+
const contextRuleIds = [0];
|
|
220
|
+
const digest = (0, oz_auth_ts_1.authDigest)((0, oz_auth_ts_1.signaturePayload)(args.networkPassphrase, original.credentials().address().nonce(), validUntilLedger, original.rootInvocation()), contextRuleIds);
|
|
221
|
+
const authEntries = [
|
|
222
|
+
(0, oz_auth_ts_1.accountEntry)(original, validUntilLedger, (0, oz_auth_ts_1.authPayload)([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))),
|
|
223
|
+
...contextRuleIds.map(() => (0, oz_auth_ts_1.delegatedSignerEntry)(args.smartAccount, digest)),
|
|
224
|
+
];
|
|
225
|
+
const txWithAuth = buildTx(makeOperation(authEntries));
|
|
226
|
+
const enforcing = await args.rpc.simulateTransaction(txWithAuth);
|
|
227
|
+
if (stellar_sdk_1.rpc.Api.isSimulationError(enforcing)) {
|
|
228
|
+
throw new Error('revoke_policy: auth simulateTransaction failed');
|
|
229
|
+
}
|
|
230
|
+
const finalTx = stellar_sdk_1.rpc.assembleTransaction(txWithAuth, enforcing).build();
|
|
200
231
|
return {
|
|
201
|
-
unsignedXdr:
|
|
232
|
+
unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
|
|
202
233
|
smartAccount: args.smartAccount,
|
|
203
234
|
sourceAccount: args.sourceAccount,
|
|
204
235
|
call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
|
|
@@ -208,7 +239,7 @@ async function buildRevokePolicyXdr(args) {
|
|
|
208
239
|
};
|
|
209
240
|
}
|
|
210
241
|
/** ~25 minutes at 5s/ledger. */
|
|
211
|
-
const
|
|
242
|
+
const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300;
|
|
212
243
|
// ---- internals ----
|
|
213
244
|
/** Adapter: turn the install-policy wire `PolicyRef` shape into the core
|
|
214
245
|
* `PolicyRef` shape `buildAddContextRuleArgs` expects. Keeps the wire
|
|
@@ -246,20 +277,24 @@ function buildUnsignedTx(args) {
|
|
|
246
277
|
// its signature, and broadcasts. We do NOT sign here.
|
|
247
278
|
return tx;
|
|
248
279
|
}
|
|
249
|
-
/** Decode the install call's structured fields directly out of the
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* will
|
|
253
|
-
*
|
|
254
|
-
* a ToolError, since that would mean the XDR was tampered with (or
|
|
280
|
+
/** Decode the install call's structured fields directly out of the final
|
|
281
|
+
* assembled transaction. The bytes are the source of truth: a human reviewing
|
|
282
|
+
* the install wants to see what the wallet will actually sign, not what we
|
|
283
|
+
* think it will. The decode is deliberately strict - any shape we did not
|
|
284
|
+
* build ourselves throws, since that would mean the XDR was tampered with (or
|
|
255
285
|
* the encoder changed and this descriptor lags).
|
|
256
286
|
*
|
|
257
287
|
* `expectedInstallNonce` is a fallback for the rare case where the
|
|
258
288
|
* install has no interpreter policy (no `install_nonce` to read); the
|
|
259
289
|
* caller already knows it. */
|
|
260
|
-
function decodeInstallCallDescribes(tx,
|
|
290
|
+
function decodeInstallCallDescribes(tx, expectedInstallNonce) {
|
|
291
|
+
const operations = tx.toEnvelope().v1().tx().operations();
|
|
292
|
+
if (operations.length !== 1 || !operations[0]) {
|
|
293
|
+
throw new Error(`install_policy: final transaction has ${operations.length} operations, expected 1`);
|
|
294
|
+
}
|
|
295
|
+
const op = operations[0];
|
|
261
296
|
const hostFn = op.body().invokeHostFunctionOp()?.hostFunction();
|
|
262
|
-
if (
|
|
297
|
+
if (hostFn?.switch().name !== 'hostFunctionTypeInvokeContract') {
|
|
263
298
|
throw new Error('install_policy: built op is not an invokeHostFunction(invokeContract(...)) call');
|
|
264
299
|
}
|
|
265
300
|
const invokeArgs = hostFn.invokeContract();
|
|
@@ -412,10 +447,6 @@ function decodeInstallCallDescribes(tx, op, expectedInstallNonce) {
|
|
|
412
447
|
// `observedInstallNonce` is the nonce baked into whichever interpreter
|
|
413
448
|
// policy is present; when none, fall back to the caller-supplied value.
|
|
414
449
|
const installNonce = observedInstallNonce ?? expectedInstallNonce;
|
|
415
|
-
// Touch `tx` so the linter does not flag it as unused - the parameter
|
|
416
|
-
// documents that the caller built this Transaction; we re-parse the
|
|
417
|
-
// operation off it as a sanity check.
|
|
418
|
-
void tx;
|
|
419
450
|
return {
|
|
420
451
|
targetContract,
|
|
421
452
|
fnName: 'add_context_rule',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { xdr } from '@stellar/stellar-sdk';
|
|
2
|
+
/** `Signer::Delegated(addr)`. */
|
|
3
|
+
export declare const delegatedSigner: (a: string) => xdr.ScVal;
|
|
4
|
+
/** The standard Soroban authorization preimage hash for one entry. */
|
|
5
|
+
export declare function signaturePayload(networkPassphrase: string, nonce: xdr.Int64, signatureExpirationLedger: number, invocation: xdr.SorobanAuthorizedInvocation): Buffer;
|
|
6
|
+
/**
|
|
7
|
+
* `sha256(signature_payload || xdr(context_rule_ids))`.
|
|
8
|
+
*
|
|
9
|
+
* OZ binds the selected rule ids into the digest so a signature for one rule
|
|
10
|
+
* cannot be replayed against another.
|
|
11
|
+
*/
|
|
12
|
+
export declare function authDigest(payload: Buffer, contextRuleIds: number[]): Buffer;
|
|
13
|
+
/** `AuthPayload { signers, context_rule_ids }` - the account's "signature". */
|
|
14
|
+
export declare function authPayload(signerAddresses: string[], contextRuleIds: number[], signatureFor: (addr: string) => Buffer): xdr.ScVal;
|
|
15
|
+
/**
|
|
16
|
+
* The nested entry a `Delegated` signer needs.
|
|
17
|
+
*
|
|
18
|
+
* `require_auth_for_args` authorizes the CURRENT frame, which while
|
|
19
|
+
* `__check_auth` is running is the account contract executing `__check_auth`,
|
|
20
|
+
* with the digest as its single argument.
|
|
21
|
+
*
|
|
22
|
+
* The signer is the transaction source here, so source-account credentials
|
|
23
|
+
* carry it and no separate signature is required.
|
|
24
|
+
*/
|
|
25
|
+
export declare function delegatedSignerEntry(accountId: string, digest: Buffer): xdr.SorobanAuthorizationEntry;
|
|
26
|
+
/** Rebuild the account's entry with the AuthPayload in the signature slot. */
|
|
27
|
+
export declare function accountEntry(original: xdr.SorobanAuthorizationEntry, signatureExpirationLedger: number, payload: xdr.ScVal): xdr.SorobanAuthorizationEntry;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Build the authorization entries OpenZeppelin's smart account requires.
|
|
3
|
+
//
|
|
4
|
+
// This is the piece that makes an end-to-end test of the account -> policy
|
|
5
|
+
// path possible. Without it, every attempt to exercise `__check_auth`
|
|
6
|
+
// produces a false positive: mocked auth skips `__check_auth` entirely,
|
|
7
|
+
// direct invocation is rejected by the host, and recording-mode simulation
|
|
8
|
+
// does not verify auth at all.
|
|
9
|
+
//
|
|
10
|
+
// Two entries are needed per call:
|
|
11
|
+
//
|
|
12
|
+
// 1. The ACCOUNT's entry. Its `signature` slot is not a signature - OZ puts
|
|
13
|
+
// an `AuthPayload { signers, context_rule_ids }` there, which
|
|
14
|
+
// `__check_auth` receives as its `signatures` argument.
|
|
15
|
+
//
|
|
16
|
+
// 2. One entry per DELEGATED signer. `do_check_auth` calls
|
|
17
|
+
// `addr.require_auth_for_args((auth_digest,))` for each, which is a
|
|
18
|
+
// nested authorization requirement the host will not record during
|
|
19
|
+
// simulation (it never runs `__check_auth` in recording mode), so it has
|
|
20
|
+
// to be constructed by hand.
|
|
21
|
+
//
|
|
22
|
+
// The digest OZ binds is NOT the raw auth payload hash:
|
|
23
|
+
//
|
|
24
|
+
// auth_digest = sha256(signature_payload || xdr(context_rule_ids))
|
|
25
|
+
//
|
|
26
|
+
// where `signature_payload` is the standard Soroban authorization preimage
|
|
27
|
+
// hash. See `do_check_auth` in
|
|
28
|
+
// stellar-contracts/packages/accounts/src/smart_account/storage.rs.
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.delegatedSigner = void 0;
|
|
31
|
+
exports.signaturePayload = signaturePayload;
|
|
32
|
+
exports.authDigest = authDigest;
|
|
33
|
+
exports.authPayload = authPayload;
|
|
34
|
+
exports.delegatedSignerEntry = delegatedSignerEntry;
|
|
35
|
+
exports.accountEntry = accountEntry;
|
|
36
|
+
const stellar_sdk_1 = require("@stellar/stellar-sdk");
|
|
37
|
+
const sym = (s) => stellar_sdk_1.xdr.ScVal.scvSymbol(s);
|
|
38
|
+
const u32 = (n) => stellar_sdk_1.xdr.ScVal.scvU32(n);
|
|
39
|
+
const vec = (i) => stellar_sdk_1.xdr.ScVal.scvVec(i);
|
|
40
|
+
const bytes = (b) => stellar_sdk_1.xdr.ScVal.scvBytes(b);
|
|
41
|
+
function struct(fields) {
|
|
42
|
+
return stellar_sdk_1.xdr.ScVal.scvMap(Object.keys(fields)
|
|
43
|
+
.sort()
|
|
44
|
+
.map((k) => new stellar_sdk_1.xdr.ScMapEntry({ key: sym(k), val: fields[k] })));
|
|
45
|
+
}
|
|
46
|
+
/** `Signer::Delegated(addr)`. */
|
|
47
|
+
const delegatedSigner = (a) => vec([sym('Delegated'), new stellar_sdk_1.Address(a).toScVal()]);
|
|
48
|
+
exports.delegatedSigner = delegatedSigner;
|
|
49
|
+
/** The standard Soroban authorization preimage hash for one entry. */
|
|
50
|
+
function signaturePayload(networkPassphrase, nonce, signatureExpirationLedger, invocation) {
|
|
51
|
+
const preimage = stellar_sdk_1.xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(new stellar_sdk_1.xdr.HashIdPreimageSorobanAuthorization({
|
|
52
|
+
networkId: (0, stellar_sdk_1.hash)(Buffer.from(networkPassphrase)),
|
|
53
|
+
nonce,
|
|
54
|
+
signatureExpirationLedger,
|
|
55
|
+
invocation,
|
|
56
|
+
}));
|
|
57
|
+
return (0, stellar_sdk_1.hash)(preimage.toXDR());
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `sha256(signature_payload || xdr(context_rule_ids))`.
|
|
61
|
+
*
|
|
62
|
+
* OZ binds the selected rule ids into the digest so a signature for one rule
|
|
63
|
+
* cannot be replayed against another.
|
|
64
|
+
*/
|
|
65
|
+
function authDigest(payload, contextRuleIds) {
|
|
66
|
+
const idsXdr = vec(contextRuleIds.map(u32)).toXDR();
|
|
67
|
+
return (0, stellar_sdk_1.hash)(Buffer.concat([payload, idsXdr]));
|
|
68
|
+
}
|
|
69
|
+
/** `AuthPayload { signers, context_rule_ids }` - the account's "signature". */
|
|
70
|
+
function authPayload(signerAddresses, contextRuleIds, signatureFor) {
|
|
71
|
+
return struct({
|
|
72
|
+
signers: stellar_sdk_1.xdr.ScVal.scvMap(signerAddresses
|
|
73
|
+
.map((a) => new stellar_sdk_1.xdr.ScMapEntry({ key: (0, exports.delegatedSigner)(a), val: bytes(signatureFor(a)) }))
|
|
74
|
+
// Host maps must be sorted by key.
|
|
75
|
+
.sort((x, y) => Buffer.compare(x.key().toXDR(), y.key().toXDR()))),
|
|
76
|
+
context_rule_ids: vec(contextRuleIds.map(u32)),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The nested entry a `Delegated` signer needs.
|
|
81
|
+
*
|
|
82
|
+
* `require_auth_for_args` authorizes the CURRENT frame, which while
|
|
83
|
+
* `__check_auth` is running is the account contract executing `__check_auth`,
|
|
84
|
+
* with the digest as its single argument.
|
|
85
|
+
*
|
|
86
|
+
* The signer is the transaction source here, so source-account credentials
|
|
87
|
+
* carry it and no separate signature is required.
|
|
88
|
+
*/
|
|
89
|
+
function delegatedSignerEntry(accountId, digest) {
|
|
90
|
+
return new stellar_sdk_1.xdr.SorobanAuthorizationEntry({
|
|
91
|
+
credentials: stellar_sdk_1.xdr.SorobanCredentials.sorobanCredentialsSourceAccount(),
|
|
92
|
+
rootInvocation: new stellar_sdk_1.xdr.SorobanAuthorizedInvocation({
|
|
93
|
+
function: stellar_sdk_1.xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(new stellar_sdk_1.xdr.InvokeContractArgs({
|
|
94
|
+
contractAddress: new stellar_sdk_1.Address(accountId).toScAddress(),
|
|
95
|
+
functionName: '__check_auth',
|
|
96
|
+
args: [bytes(digest)],
|
|
97
|
+
})),
|
|
98
|
+
subInvocations: [],
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Rebuild the account's entry with the AuthPayload in the signature slot. */
|
|
103
|
+
function accountEntry(original, signatureExpirationLedger, payload) {
|
|
104
|
+
const creds = original.credentials().address();
|
|
105
|
+
return new stellar_sdk_1.xdr.SorobanAuthorizationEntry({
|
|
106
|
+
credentials: stellar_sdk_1.xdr.SorobanCredentials.sorobanCredentialsAddress(new stellar_sdk_1.xdr.SorobanAddressCredentials({
|
|
107
|
+
address: creds.address(),
|
|
108
|
+
nonce: creds.nonce(),
|
|
109
|
+
signatureExpirationLedger,
|
|
110
|
+
signature: payload,
|
|
111
|
+
})),
|
|
112
|
+
rootInvocation: original.rootInvocation(),
|
|
113
|
+
});
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-synth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Off-chain TypeScript synthesis core for the OZ Accounts Policy Builder. Records Soroban transactions, synthesises the minimal policy that permits exactly that flow, verifies it, and returns an unsigned install transaction.",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
xdr,
|
|
32
32
|
} from '@stellar/stellar-sdk'
|
|
33
33
|
import { buildAddContextRuleArgs, DEFAULT_GRAMMAR_VERSION } from './build-add-context-rule.ts'
|
|
34
|
+
import {
|
|
35
|
+
accountEntry,
|
|
36
|
+
authDigest,
|
|
37
|
+
authPayload,
|
|
38
|
+
delegatedSignerEntry,
|
|
39
|
+
signaturePayload,
|
|
40
|
+
} from './oz-auth.ts'
|
|
34
41
|
|
|
35
42
|
/** Minimal Soroban RPC surface the install pipeline needs. Test seams
|
|
36
43
|
* inject a stub so the builder stays deterministic; production builds
|
|
@@ -265,36 +272,33 @@ export async function buildInstallPolicyXdr(
|
|
|
265
272
|
}
|
|
266
273
|
)
|
|
267
274
|
|
|
268
|
-
// 2.
|
|
275
|
+
// 2. Fetch the source sequence, then build the recording transaction with a
|
|
276
|
+
// bare host call. The recording pass assigns the smart-account auth nonce
|
|
277
|
+
// and root invocation needed to construct OZ's AuthPayload.
|
|
269
278
|
const source = await args.rpc.getAccount(args.sourceAccount)
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
functionName: 'add_context_rule',
|
|
280
|
-
args: [...callArgs],
|
|
281
|
-
})
|
|
282
|
-
),
|
|
283
|
-
auth: [],
|
|
284
|
-
})
|
|
279
|
+
const hostFunction = xdr.HostFunction.hostFunctionTypeInvokeContract(
|
|
280
|
+
new xdr.InvokeContractArgs({
|
|
281
|
+
contractAddress: new Address(args.smartAccount).toScAddress(),
|
|
282
|
+
functionName: 'add_context_rule',
|
|
283
|
+
args: [...callArgs],
|
|
284
|
+
})
|
|
285
|
+
)
|
|
286
|
+
const makeOperation = (auth: xdr.SorobanAuthorizationEntry[] = []) =>
|
|
287
|
+
Operation.invokeHostFunction({ func: hostFunction, auth })
|
|
285
288
|
const baseFee = args.baseFee !== undefined ? String(args.baseFee) : BASE_FEE
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
289
|
+
const buildTx = (op: xdr.Operation) =>
|
|
290
|
+
buildUnsignedTx({
|
|
291
|
+
sourceAccount: args.sourceAccount,
|
|
292
|
+
sequence: source.sequenceNumber(),
|
|
293
|
+
fee: baseFee,
|
|
294
|
+
networkPassphrase: args.networkPassphrase,
|
|
295
|
+
op,
|
|
296
|
+
})
|
|
297
|
+
const recordingTx = buildTx(makeOperation())
|
|
298
|
+
|
|
299
|
+
// 3. Recording simulation: capture the smart account's nonce and invocation
|
|
300
|
+
// tree. Nothing is signed here.
|
|
301
|
+
const recorded = await args.rpc.simulateTransaction(recordingTx)
|
|
298
302
|
if (rpc.Api.isSimulationError(recorded)) {
|
|
299
303
|
// Short, stable reason. The full `simulateTransaction` error (which
|
|
300
304
|
// carries host + URL detail) stays in the SDK's own logs - never
|
|
@@ -314,15 +318,48 @@ export async function buildInstallPolicyXdr(
|
|
|
314
318
|
)
|
|
315
319
|
}
|
|
316
320
|
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
// 4. `add_context_rule` is authorised by the deploy-time admin rule (rule 0).
|
|
322
|
+
// The delegated signer uses source-account credentials, so its signature
|
|
323
|
+
// bytes stay empty and the ordinary transaction-envelope signature covers
|
|
324
|
+
// the call when the consumer signs it.
|
|
325
|
+
const validUntilLedger =
|
|
326
|
+
(await args.rpc.getLatestLedger()).sequence +
|
|
327
|
+
(args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS)
|
|
328
|
+
const contextRuleIds = [0]
|
|
329
|
+
const digest = authDigest(
|
|
330
|
+
signaturePayload(
|
|
331
|
+
args.networkPassphrase,
|
|
332
|
+
original.credentials().address().nonce(),
|
|
333
|
+
validUntilLedger,
|
|
334
|
+
original.rootInvocation()
|
|
335
|
+
),
|
|
336
|
+
contextRuleIds
|
|
337
|
+
)
|
|
338
|
+
const authEntries = [
|
|
339
|
+
accountEntry(
|
|
340
|
+
original,
|
|
341
|
+
validUntilLedger,
|
|
342
|
+
authPayload([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))
|
|
343
|
+
),
|
|
344
|
+
...contextRuleIds.map(() => delegatedSignerEntry(args.smartAccount, digest)),
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
// 5. Simulate again with the OZ entries already attached. The SDK preserves
|
|
348
|
+
// existing auth during assembly and adds the simulated Soroban footprint +
|
|
349
|
+
// resource fee, yielding a complete unsigned envelope.
|
|
350
|
+
const txWithAuth = buildTx(makeOperation(authEntries))
|
|
351
|
+
const enforcing = await args.rpc.simulateTransaction(txWithAuth)
|
|
352
|
+
if (rpc.Api.isSimulationError(enforcing)) {
|
|
353
|
+
throw new Error('install_policy: auth simulateTransaction failed')
|
|
354
|
+
}
|
|
355
|
+
const finalTx = rpc.assembleTransaction(txWithAuth, enforcing).build()
|
|
356
|
+
|
|
357
|
+
// 6. Decode the structured description FROM the final assembled transaction.
|
|
358
|
+
// The human approval binds to the exact bytes the wallet will sign.
|
|
359
|
+
const describes = decodeInstallCallDescribes(finalTx, args.installNonce)
|
|
323
360
|
|
|
324
361
|
return {
|
|
325
|
-
unsignedXdr:
|
|
362
|
+
unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
|
|
326
363
|
smartAccount: args.smartAccount,
|
|
327
364
|
sourceAccount: args.sourceAccount,
|
|
328
365
|
call: { contract: args.smartAccount, fn: 'add_context_rule' },
|
|
@@ -360,30 +397,26 @@ export async function buildRevokePolicyXdr(args: {
|
|
|
360
397
|
authValidUntilLedgers?: number
|
|
361
398
|
}): Promise<BuildRevokePolicyResult> {
|
|
362
399
|
const source = await args.rpc.getAccount(args.sourceAccount)
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
args: [xdr.ScVal.scvU32(args.ruleId)],
|
|
373
|
-
})
|
|
374
|
-
),
|
|
375
|
-
auth: [],
|
|
376
|
-
})
|
|
400
|
+
const hostFunction = xdr.HostFunction.hostFunctionTypeInvokeContract(
|
|
401
|
+
new xdr.InvokeContractArgs({
|
|
402
|
+
contractAddress: new Address(args.smartAccount).toScAddress(),
|
|
403
|
+
functionName: 'remove_context_rule',
|
|
404
|
+
args: [xdr.ScVal.scvU32(args.ruleId)],
|
|
405
|
+
})
|
|
406
|
+
)
|
|
407
|
+
const makeOperation = (auth: xdr.SorobanAuthorizationEntry[] = []) =>
|
|
408
|
+
Operation.invokeHostFunction({ func: hostFunction, auth })
|
|
377
409
|
const baseFee = args.baseFee !== undefined ? String(args.baseFee) : BASE_FEE
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
410
|
+
const buildTx = (op: xdr.Operation) =>
|
|
411
|
+
buildUnsignedTx({
|
|
412
|
+
sourceAccount: args.sourceAccount,
|
|
413
|
+
sequence: source.sequenceNumber(),
|
|
414
|
+
fee: baseFee,
|
|
415
|
+
networkPassphrase: args.networkPassphrase,
|
|
416
|
+
op,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const recorded = await args.rpc.simulateTransaction(buildTx(makeOperation()))
|
|
387
420
|
if (rpc.Api.isSimulationError(recorded)) {
|
|
388
421
|
// Short, stable reason. The full `simulateTransaction` error (which
|
|
389
422
|
// carries host + URL detail) stays in the SDK's own logs - never
|
|
@@ -403,8 +436,41 @@ export async function buildRevokePolicyXdr(args: {
|
|
|
403
436
|
)
|
|
404
437
|
}
|
|
405
438
|
|
|
439
|
+
// Removal is master-only, so the deploy-time admin rule authorises it. The
|
|
440
|
+
// recorded rootInvocation still includes remove_context_rule(ruleId), binding
|
|
441
|
+
// the auth payload to the exact rule being removed. As with install,
|
|
442
|
+
// source-account credentials carry the delegated signer entry and the
|
|
443
|
+
// consumer supplies only the ordinary envelope signature.
|
|
444
|
+
const validUntilLedger =
|
|
445
|
+
(await args.rpc.getLatestLedger()).sequence +
|
|
446
|
+
(args.authValidUntilLedgers ?? DEFAULT_AUTH_VALID_UNTIL_LEDGERS)
|
|
447
|
+
const contextRuleIds = [0]
|
|
448
|
+
const digest = authDigest(
|
|
449
|
+
signaturePayload(
|
|
450
|
+
args.networkPassphrase,
|
|
451
|
+
original.credentials().address().nonce(),
|
|
452
|
+
validUntilLedger,
|
|
453
|
+
original.rootInvocation()
|
|
454
|
+
),
|
|
455
|
+
contextRuleIds
|
|
456
|
+
)
|
|
457
|
+
const authEntries = [
|
|
458
|
+
accountEntry(
|
|
459
|
+
original,
|
|
460
|
+
validUntilLedger,
|
|
461
|
+
authPayload([args.sourceAccount], contextRuleIds, () => Buffer.alloc(0))
|
|
462
|
+
),
|
|
463
|
+
...contextRuleIds.map(() => delegatedSignerEntry(args.smartAccount, digest)),
|
|
464
|
+
]
|
|
465
|
+
const txWithAuth = buildTx(makeOperation(authEntries))
|
|
466
|
+
const enforcing = await args.rpc.simulateTransaction(txWithAuth)
|
|
467
|
+
if (rpc.Api.isSimulationError(enforcing)) {
|
|
468
|
+
throw new Error('revoke_policy: auth simulateTransaction failed')
|
|
469
|
+
}
|
|
470
|
+
const finalTx = rpc.assembleTransaction(txWithAuth, enforcing).build()
|
|
471
|
+
|
|
406
472
|
return {
|
|
407
|
-
unsignedXdr:
|
|
473
|
+
unsignedXdr: finalTx.toEnvelope().toXDR().toString('base64'),
|
|
408
474
|
smartAccount: args.smartAccount,
|
|
409
475
|
sourceAccount: args.sourceAccount,
|
|
410
476
|
call: { contract: args.smartAccount, fn: 'remove_context_rule', ruleId: args.ruleId },
|
|
@@ -425,7 +491,7 @@ export interface BuildRevokePolicyResult {
|
|
|
425
491
|
}
|
|
426
492
|
|
|
427
493
|
/** ~25 minutes at 5s/ledger. */
|
|
428
|
-
const
|
|
494
|
+
const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300
|
|
429
495
|
|
|
430
496
|
// ---- internals ----
|
|
431
497
|
|
|
@@ -476,12 +542,11 @@ function buildUnsignedTx(args: {
|
|
|
476
542
|
return tx
|
|
477
543
|
}
|
|
478
544
|
|
|
479
|
-
/** Decode the install call's structured fields directly out of the
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
* will
|
|
483
|
-
*
|
|
484
|
-
* a ToolError, since that would mean the XDR was tampered with (or
|
|
545
|
+
/** Decode the install call's structured fields directly out of the final
|
|
546
|
+
* assembled transaction. The bytes are the source of truth: a human reviewing
|
|
547
|
+
* the install wants to see what the wallet will actually sign, not what we
|
|
548
|
+
* think it will. The decode is deliberately strict - any shape we did not
|
|
549
|
+
* build ourselves throws, since that would mean the XDR was tampered with (or
|
|
485
550
|
* the encoder changed and this descriptor lags).
|
|
486
551
|
*
|
|
487
552
|
* `expectedInstallNonce` is a fallback for the rare case where the
|
|
@@ -489,11 +554,17 @@ function buildUnsignedTx(args: {
|
|
|
489
554
|
* caller already knows it. */
|
|
490
555
|
function decodeInstallCallDescribes(
|
|
491
556
|
tx: Transaction,
|
|
492
|
-
op: xdr.Operation,
|
|
493
557
|
expectedInstallNonce: number
|
|
494
558
|
): InstallCallDescribes {
|
|
559
|
+
const operations = tx.toEnvelope().v1().tx().operations()
|
|
560
|
+
if (operations.length !== 1 || !operations[0]) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`install_policy: final transaction has ${operations.length} operations, expected 1`
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
const op = operations[0]
|
|
495
566
|
const hostFn = op.body().invokeHostFunctionOp()?.hostFunction()
|
|
496
|
-
if (
|
|
567
|
+
if (hostFn?.switch().name !== 'hostFunctionTypeInvokeContract') {
|
|
497
568
|
throw new Error(
|
|
498
569
|
'install_policy: built op is not an invokeHostFunction(invokeContract(...)) call'
|
|
499
570
|
)
|
|
@@ -661,10 +732,6 @@ function decodeInstallCallDescribes(
|
|
|
661
732
|
// `observedInstallNonce` is the nonce baked into whichever interpreter
|
|
662
733
|
// policy is present; when none, fall back to the caller-supplied value.
|
|
663
734
|
const installNonce = observedInstallNonce ?? expectedInstallNonce
|
|
664
|
-
// Touch `tx` so the linter does not flag it as unused - the parameter
|
|
665
|
-
// documents that the caller built this Transaction; we re-parse the
|
|
666
|
-
// operation off it as a sanity check.
|
|
667
|
-
void tx
|
|
668
735
|
return {
|
|
669
736
|
targetContract,
|
|
670
737
|
fnName: 'add_context_rule',
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Build the authorization entries OpenZeppelin's smart account requires.
|
|
2
|
+
//
|
|
3
|
+
// This is the piece that makes an end-to-end test of the account -> policy
|
|
4
|
+
// path possible. Without it, every attempt to exercise `__check_auth`
|
|
5
|
+
// produces a false positive: mocked auth skips `__check_auth` entirely,
|
|
6
|
+
// direct invocation is rejected by the host, and recording-mode simulation
|
|
7
|
+
// does not verify auth at all.
|
|
8
|
+
//
|
|
9
|
+
// Two entries are needed per call:
|
|
10
|
+
//
|
|
11
|
+
// 1. The ACCOUNT's entry. Its `signature` slot is not a signature - OZ puts
|
|
12
|
+
// an `AuthPayload { signers, context_rule_ids }` there, which
|
|
13
|
+
// `__check_auth` receives as its `signatures` argument.
|
|
14
|
+
//
|
|
15
|
+
// 2. One entry per DELEGATED signer. `do_check_auth` calls
|
|
16
|
+
// `addr.require_auth_for_args((auth_digest,))` for each, which is a
|
|
17
|
+
// nested authorization requirement the host will not record during
|
|
18
|
+
// simulation (it never runs `__check_auth` in recording mode), so it has
|
|
19
|
+
// to be constructed by hand.
|
|
20
|
+
//
|
|
21
|
+
// The digest OZ binds is NOT the raw auth payload hash:
|
|
22
|
+
//
|
|
23
|
+
// auth_digest = sha256(signature_payload || xdr(context_rule_ids))
|
|
24
|
+
//
|
|
25
|
+
// where `signature_payload` is the standard Soroban authorization preimage
|
|
26
|
+
// hash. See `do_check_auth` in
|
|
27
|
+
// stellar-contracts/packages/accounts/src/smart_account/storage.rs.
|
|
28
|
+
|
|
29
|
+
import { Address, hash, xdr } from '@stellar/stellar-sdk'
|
|
30
|
+
|
|
31
|
+
const sym = (s: string) => xdr.ScVal.scvSymbol(s)
|
|
32
|
+
const u32 = (n: number) => xdr.ScVal.scvU32(n)
|
|
33
|
+
const vec = (i: xdr.ScVal[]) => xdr.ScVal.scvVec(i)
|
|
34
|
+
const bytes = (b: Buffer) => xdr.ScVal.scvBytes(b)
|
|
35
|
+
|
|
36
|
+
function struct(fields: Record<string, xdr.ScVal>): xdr.ScVal {
|
|
37
|
+
return xdr.ScVal.scvMap(
|
|
38
|
+
Object.keys(fields)
|
|
39
|
+
.sort()
|
|
40
|
+
.map((k) => new xdr.ScMapEntry({ key: sym(k), val: fields[k]! }))
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `Signer::Delegated(addr)`. */
|
|
45
|
+
export const delegatedSigner = (a: string) => vec([sym('Delegated'), new Address(a).toScVal()])
|
|
46
|
+
|
|
47
|
+
/** The standard Soroban authorization preimage hash for one entry. */
|
|
48
|
+
export function signaturePayload(
|
|
49
|
+
networkPassphrase: string,
|
|
50
|
+
nonce: xdr.Int64,
|
|
51
|
+
signatureExpirationLedger: number,
|
|
52
|
+
invocation: xdr.SorobanAuthorizedInvocation
|
|
53
|
+
): Buffer {
|
|
54
|
+
const preimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(
|
|
55
|
+
new xdr.HashIdPreimageSorobanAuthorization({
|
|
56
|
+
networkId: hash(Buffer.from(networkPassphrase)),
|
|
57
|
+
nonce,
|
|
58
|
+
signatureExpirationLedger,
|
|
59
|
+
invocation,
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
return hash(preimage.toXDR())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `sha256(signature_payload || xdr(context_rule_ids))`.
|
|
67
|
+
*
|
|
68
|
+
* OZ binds the selected rule ids into the digest so a signature for one rule
|
|
69
|
+
* cannot be replayed against another.
|
|
70
|
+
*/
|
|
71
|
+
export function authDigest(payload: Buffer, contextRuleIds: number[]): Buffer {
|
|
72
|
+
const idsXdr = vec(contextRuleIds.map(u32)).toXDR()
|
|
73
|
+
return hash(Buffer.concat([payload, idsXdr]))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `AuthPayload { signers, context_rule_ids }` - the account's "signature". */
|
|
77
|
+
export function authPayload(
|
|
78
|
+
signerAddresses: string[],
|
|
79
|
+
contextRuleIds: number[],
|
|
80
|
+
signatureFor: (addr: string) => Buffer
|
|
81
|
+
): xdr.ScVal {
|
|
82
|
+
return struct({
|
|
83
|
+
signers: xdr.ScVal.scvMap(
|
|
84
|
+
signerAddresses
|
|
85
|
+
.map((a) => new xdr.ScMapEntry({ key: delegatedSigner(a), val: bytes(signatureFor(a)) }))
|
|
86
|
+
// Host maps must be sorted by key.
|
|
87
|
+
.sort((x, y) => Buffer.compare(x.key().toXDR(), y.key().toXDR()))
|
|
88
|
+
),
|
|
89
|
+
context_rule_ids: vec(contextRuleIds.map(u32)),
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The nested entry a `Delegated` signer needs.
|
|
95
|
+
*
|
|
96
|
+
* `require_auth_for_args` authorizes the CURRENT frame, which while
|
|
97
|
+
* `__check_auth` is running is the account contract executing `__check_auth`,
|
|
98
|
+
* with the digest as its single argument.
|
|
99
|
+
*
|
|
100
|
+
* The signer is the transaction source here, so source-account credentials
|
|
101
|
+
* carry it and no separate signature is required.
|
|
102
|
+
*/
|
|
103
|
+
export function delegatedSignerEntry(
|
|
104
|
+
accountId: string,
|
|
105
|
+
digest: Buffer
|
|
106
|
+
): xdr.SorobanAuthorizationEntry {
|
|
107
|
+
return new xdr.SorobanAuthorizationEntry({
|
|
108
|
+
credentials: xdr.SorobanCredentials.sorobanCredentialsSourceAccount(),
|
|
109
|
+
rootInvocation: new xdr.SorobanAuthorizedInvocation({
|
|
110
|
+
function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(
|
|
111
|
+
new xdr.InvokeContractArgs({
|
|
112
|
+
contractAddress: new Address(accountId).toScAddress(),
|
|
113
|
+
functionName: '__check_auth',
|
|
114
|
+
args: [bytes(digest)],
|
|
115
|
+
})
|
|
116
|
+
),
|
|
117
|
+
subInvocations: [],
|
|
118
|
+
}),
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Rebuild the account's entry with the AuthPayload in the signature slot. */
|
|
123
|
+
export function accountEntry(
|
|
124
|
+
original: xdr.SorobanAuthorizationEntry,
|
|
125
|
+
signatureExpirationLedger: number,
|
|
126
|
+
payload: xdr.ScVal
|
|
127
|
+
): xdr.SorobanAuthorizationEntry {
|
|
128
|
+
const creds = original.credentials().address()
|
|
129
|
+
return new xdr.SorobanAuthorizationEntry({
|
|
130
|
+
credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(
|
|
131
|
+
new xdr.SorobanAddressCredentials({
|
|
132
|
+
address: creds.address(),
|
|
133
|
+
nonce: creds.nonce(),
|
|
134
|
+
signatureExpirationLedger,
|
|
135
|
+
signature: payload,
|
|
136
|
+
})
|
|
137
|
+
),
|
|
138
|
+
rootInvocation: original.rootInvocation(),
|
|
139
|
+
})
|
|
140
|
+
}
|