@mysten-incubation/sui-agent-pass 0.0.0 → 0.1.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/LICENSE +201 -0
- package/README.md +423 -4
- package/deployments/README.md +28 -0
- package/deployments/mainnet.json +65 -0
- package/deployments/testnet.json +69 -0
- package/dist/activation.js +287 -0
- package/dist/agent.js +409 -0
- package/dist/chain.js +763 -0
- package/dist/cli.js +245 -0
- package/dist/describe.js +352 -0
- package/dist/domain.js +393 -0
- package/dist/explorer.js +143 -0
- package/dist/float.js +80 -0
- package/dist/mcp.js +180 -0
- package/dist/navi.js +125 -0
- package/dist/owner.js +410 -0
- package/dist/plugin.js +795 -0
- package/dist/server.js +229 -0
- package/dist/signer.js +40 -0
- package/dist/storage.js +180 -0
- package/docs/AGENT.md +207 -0
- package/docs/PLAYBOOK.md +199 -0
- package/package.json +95 -10
- package/web-dist/assets/index-GamQ8vny.js +844 -0
- package/web-dist/assets/index-IgtbvOwM.css +1 -0
- package/web-dist/assets/mount-B6ddx2sS.js +3842 -0
- package/web-dist/deployments/mainnet.json +65 -0
- package/web-dist/deployments/testnet.json +69 -0
- package/web-dist/favicon.svg +10 -0
- package/web-dist/index.html +19 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"network": "mainnet",
|
|
3
|
+
"grpcUrl": "https://fullnode.mainnet.sui.io:443",
|
|
4
|
+
"chainIdentifier": "4btiuiMPvEENsttpZC7CZ53DruC3MAgfznDbASZ7DR6S",
|
|
5
|
+
"packageId": "0x4c9010b0be6022e8a4bd91219d5174edc36d524b42d4790784035f424e01f403",
|
|
6
|
+
"publishedAt": "0x08d82880b61c531ffbe9eb9eccf3c025d6182ec50f7f61e1f6095a6b81a1f744",
|
|
7
|
+
"configId": "0xf7b33e20564b26c0685b8d86669345a0121ff6b290911a04f1ff37fcd1f5716d",
|
|
8
|
+
"siteUrl": "https://sui-agent-pass.vercel.app",
|
|
9
|
+
"coins": [
|
|
10
|
+
{
|
|
11
|
+
"type": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI",
|
|
12
|
+
"symbol": "SUI",
|
|
13
|
+
"decimals": 9
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"type": "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
|
|
17
|
+
"symbol": "USDC",
|
|
18
|
+
"decimals": 6
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"pools": [
|
|
22
|
+
{
|
|
23
|
+
"venue": "deepbook",
|
|
24
|
+
"poolId": "0xe05dafb5133bcffb8d59f4e12465dc0e9faeaa05e3e342a08fe135800e3e4407",
|
|
25
|
+
"coinA": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI",
|
|
26
|
+
"coinB": "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"venue": "cetus",
|
|
30
|
+
"poolId": "0xb8d7d9e66a60c239e7a60110efcf8de6c705580ed924d0dde141f4a0e2c90105",
|
|
31
|
+
"coinA": "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC",
|
|
32
|
+
"coinB": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"protocols": {
|
|
36
|
+
"deepbook": {
|
|
37
|
+
"packageId": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809",
|
|
38
|
+
"publishedAt": "0x0e735f8c93a95722efd73521aca7a7652c0bb71ed1daf41b26dfd7d1ff71f748"
|
|
39
|
+
},
|
|
40
|
+
"cetus": {
|
|
41
|
+
"packageId": "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb",
|
|
42
|
+
"publishedAt": "0x260693ec785a6e6c9d81d58c7d2ff72f1288ae0fa6a9725abe05a6478b11f084",
|
|
43
|
+
"configId": "0xdaa46292632c3c4d8f31f23ea0f9b36a28ff3677e9684980e4438403a67a3d8f"
|
|
44
|
+
},
|
|
45
|
+
"navi": {
|
|
46
|
+
"packageId": "0x66c91a8560cd64d73d93dd1ec7b61f3f21ad2f66553dd3d7038ca69255479bb7",
|
|
47
|
+
"publishedAt": "0x512f28261c1a293f49416d8885b8d5d32bde6dd68a99a0be36fe42b248e6833a",
|
|
48
|
+
"oraclePackageId": "0xca441b44943c16be0e6e23c5a955bb971537ea3289ae8016fbf33fffe1fd210f",
|
|
49
|
+
"storageId": "0xbb4e2f4b6205c2e2a2db47aeb4f830796ec7c005f88537ee775986639bc442fe",
|
|
50
|
+
"oracleId": "0x1568865ed9a0b5ec414220e8f79b3d04c77acc82358f6e5ae4635687392ffbef",
|
|
51
|
+
"incentiveV2Id": "0xf87a8acb8b81d14307894d12595541a73f19933f88e1326d5be349c7a6f7559c",
|
|
52
|
+
"incentiveV3Id": "0x62982dad27fb10bb314b3384d5de8d2ac2d72ab2dbeae5d801dbdb9efa816c80",
|
|
53
|
+
"reserves": {
|
|
54
|
+
"0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI": {
|
|
55
|
+
"poolId": "0x96df0fce3c471489f4debaaa762cf960b3d97820bd1f3f025ff8190730e958c5",
|
|
56
|
+
"assetId": 0
|
|
57
|
+
},
|
|
58
|
+
"0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC": {
|
|
59
|
+
"poolId": "0xa3582097b4c57630046c0c49a88bfc6b202a3ec0a9db5597c31765f7563755a8",
|
|
60
|
+
"assetId": 10
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"network": "testnet",
|
|
3
|
+
"grpcUrl": "https://fullnode.testnet.sui.io:443",
|
|
4
|
+
"chainIdentifier": "69WiPg3DAQiwdxfncX6wYQ2siKwAe6L9BZthQea3JNMD",
|
|
5
|
+
"packageId": "0xfea0b9f60c752e98e950c1994a3805ba28c5f559e3ea72da2ac3d68b90728b11",
|
|
6
|
+
"publishedAt": "0xfea0b9f60c752e98e950c1994a3805ba28c5f559e3ea72da2ac3d68b90728b11",
|
|
7
|
+
"configId": "0x3bb796cd3fe0d9b85b1db972478616e717fe5bdbd1ff864615a3995f4e1c1c60",
|
|
8
|
+
"coins": [
|
|
9
|
+
{
|
|
10
|
+
"type": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI",
|
|
11
|
+
"symbol": "SUI",
|
|
12
|
+
"decimals": 9
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "0x7ffc375a2bebedcec3753cfe4687f3bb104074eb55b014f8185ccc8ee8bf47fa::usdc::USDC",
|
|
16
|
+
"symbol": "USDC",
|
|
17
|
+
"decimals": 6
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"pools": [
|
|
21
|
+
{
|
|
22
|
+
"venue": "deepbook",
|
|
23
|
+
"poolId": "0xf95b10ec49e92a4afaa81ff679ebfd8b7e9371984d8c214b26783d9fdfc02a29",
|
|
24
|
+
"coinA": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI",
|
|
25
|
+
"coinB": "0x7ffc375a2bebedcec3753cfe4687f3bb104074eb55b014f8185ccc8ee8bf47fa::usdc::USDC"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"venue": "cetus",
|
|
29
|
+
"poolId": "0x3c779cab1582156a06aca223690bcd23f781ff3fbdda99747659ebddc2059e88",
|
|
30
|
+
"coinA": "0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI",
|
|
31
|
+
"coinB": "0x7ffc375a2bebedcec3753cfe4687f3bb104074eb55b014f8185ccc8ee8bf47fa::usdc::USDC"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"demoFaucet": {
|
|
35
|
+
"packageId": "0x7ffc375a2bebedcec3753cfe4687f3bb104074eb55b014f8185ccc8ee8bf47fa",
|
|
36
|
+
"faucetId": "0x14d07fd3eff80c868c0bab116cbc2f65c8903c77fcb41721ad6db4ba0c6480cf",
|
|
37
|
+
"coin": "0x7ffc375a2bebedcec3753cfe4687f3bb104074eb55b014f8185ccc8ee8bf47fa::usdc::USDC"
|
|
38
|
+
},
|
|
39
|
+
"protocols": {
|
|
40
|
+
"deepbook": {
|
|
41
|
+
"packageId": "0xfe5ce722543b1e3056aba9f44fd0ac69238c97baae9ac96d0bc1594dfe8ef7b1",
|
|
42
|
+
"publishedAt": "0xfe5ce722543b1e3056aba9f44fd0ac69238c97baae9ac96d0bc1594dfe8ef7b1"
|
|
43
|
+
},
|
|
44
|
+
"cetus": {
|
|
45
|
+
"packageId": "0x23200a7f55d23555844bca491e5422e495d1a9082f4fb39b868d95440655a8d9",
|
|
46
|
+
"publishedAt": "0x23200a7f55d23555844bca491e5422e495d1a9082f4fb39b868d95440655a8d9",
|
|
47
|
+
"configId": "0x9e4115923d7ba4405a9c918a1d517325a9491b7e2eb3f1e469debae3ceb690cd"
|
|
48
|
+
},
|
|
49
|
+
"navi": {
|
|
50
|
+
"packageId": "0xd75f91c29ae9b0597f82ff636940199e5004089d17991fafbabb9a180f51b5f7",
|
|
51
|
+
"publishedAt": "0xd75f91c29ae9b0597f82ff636940199e5004089d17991fafbabb9a180f51b5f7",
|
|
52
|
+
"oraclePackageId": "0x9d2c0e390e32e75b2c6be7a33d12319c912caae11874fcf6e1d79ae8b594e9a6",
|
|
53
|
+
"storageId": "0xc8b3b4882625279ce505b94902a33e4cf0364bd1ee2f52563e66f9a42de7af26",
|
|
54
|
+
"oracleId": "0xc962d8795540426c4dce1fea621d89f12f00766a6d4b0d00087fde6fefb2d343",
|
|
55
|
+
"incentiveV2Id": "0x679e3525405bb43ba27b0839dcaae2469d59225856cd6c2e8318a094efe593c5",
|
|
56
|
+
"incentiveV3Id": "0x9488d70edbfd67babcc72b8eb1f3fbf60dd47f080d830e2af27abcecdd6771c6",
|
|
57
|
+
"reserves": {
|
|
58
|
+
"0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI": {
|
|
59
|
+
"poolId": "0x3bdcd67ca6465324f0977d7912dbfb7a25cad759e03744aa760e0fa5baa31812",
|
|
60
|
+
"assetId": 0
|
|
61
|
+
},
|
|
62
|
+
"0x7ffc375a2bebedcec3753cfe4687f3bb104074eb55b014f8185ccc8ee8bf47fa::usdc::USDC": {
|
|
63
|
+
"poolId": "0x7f05ed3b912e6e1b573f8d3095cf3c437fb0fcf78d09669c504c66bceb3bf9a3",
|
|
64
|
+
"assetId": 1
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/** A wallet callback is a hint, never proof. Confirm the actual chain effects. */
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { normalizeStructTag, normalizeSuiAddress, parseStructTag } from '@mysten/sui/utils';
|
|
6
|
+
import { allowanceCoin, appType, assertChain, clientFor, discoverAllowances, readAllowance, readAllowanceCapTarget, readAuthorization, readControl, receiptType, } from './chain.js';
|
|
7
|
+
import { AGENT_GAS_NAME, healthFactorRay, SUI, } from './domain.js';
|
|
8
|
+
/** The policies the reviewed setup should have produced, in the chain reader's shape. */
|
|
9
|
+
export function expectedPolicies(d, p) {
|
|
10
|
+
const usage = { usedToday: '0', resetsAtMs: 0 };
|
|
11
|
+
const norm = (t) => normalizeStructTag(t);
|
|
12
|
+
const byType = (a, b) => a.coinType.localeCompare(b.coinType);
|
|
13
|
+
return {
|
|
14
|
+
transfers: p.transfers
|
|
15
|
+
.map((t) => ({
|
|
16
|
+
coinType: norm(t.coin),
|
|
17
|
+
recipients: [...t.recipients]
|
|
18
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
19
|
+
.map((r) => ({ name: r.name, address: r.address })),
|
|
20
|
+
perTransfer: t.perTransfer,
|
|
21
|
+
daily: t.daily,
|
|
22
|
+
usage,
|
|
23
|
+
}))
|
|
24
|
+
.sort(byType),
|
|
25
|
+
swaps: p.swaps
|
|
26
|
+
.map((s) => ({
|
|
27
|
+
coinIn: norm(s.coinIn),
|
|
28
|
+
coinOut: norm(s.coinOut),
|
|
29
|
+
venues: s.pools,
|
|
30
|
+
perTrade: s.perTrade,
|
|
31
|
+
daily: s.daily,
|
|
32
|
+
minPrice: s.minPrice,
|
|
33
|
+
usage,
|
|
34
|
+
}))
|
|
35
|
+
.sort((a, b) => (a.coinIn + a.coinOut).localeCompare(b.coinIn + b.coinOut)),
|
|
36
|
+
lending: p.lending
|
|
37
|
+
.map((l) => {
|
|
38
|
+
const reserve = d.protocols.navi.reserves[norm(l.coin)];
|
|
39
|
+
if (!reserve)
|
|
40
|
+
throw new Error(`NAVI has no reserve for ${l.coin} in this deployment`);
|
|
41
|
+
return {
|
|
42
|
+
coinType: norm(l.coin),
|
|
43
|
+
poolId: reserve.poolId,
|
|
44
|
+
asset: reserve.assetId,
|
|
45
|
+
allowBorrow: l.allowBorrow,
|
|
46
|
+
perAction: l.perAction,
|
|
47
|
+
daily: l.daily,
|
|
48
|
+
minHealthFactor: healthFactorRay(l.minHealthFactor),
|
|
49
|
+
usage,
|
|
50
|
+
};
|
|
51
|
+
})
|
|
52
|
+
.sort(byType),
|
|
53
|
+
staking: p.staking
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
validators: p.staking.validators,
|
|
57
|
+
perStake: p.staking.perStake,
|
|
58
|
+
daily: p.staking.daily,
|
|
59
|
+
usage,
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
: [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Zeroes reset times, which depend on when the reader ran, before comparing rules. */
|
|
66
|
+
function rulesOnly(p) {
|
|
67
|
+
const strip = (x) => ({
|
|
68
|
+
...x,
|
|
69
|
+
usage: { ...x.usage, resetsAtMs: 0 },
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
transfers: p.transfers.map(strip),
|
|
73
|
+
swaps: p.swaps.map(strip),
|
|
74
|
+
lending: p.lending.map(strip),
|
|
75
|
+
staking: p.staking.map(strip),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function checkControl(control, d, setup, owner, session) {
|
|
79
|
+
if (control.owner !== owner ||
|
|
80
|
+
control.agent !== session ||
|
|
81
|
+
control.name !== setup.name ||
|
|
82
|
+
control.agentExpiresMs !== String(setup.expiresMs) ||
|
|
83
|
+
Number(control.agentExpiresMs) <= control.chainTimeMs)
|
|
84
|
+
throw new Error('On-chain owner, agent or term does not match the reviewed setup');
|
|
85
|
+
if (!isDeepStrictEqual(rulesOnly(control.policies), expectedPolicies(d, setup.policy)))
|
|
86
|
+
throw new Error('On-chain policies do not match the reviewed setup');
|
|
87
|
+
// The agent's lending runs on the pass's NAVI account; it exists exactly when reviewed.
|
|
88
|
+
if (setup.policy.lending.length > 0 !== !!control.naviPosition)
|
|
89
|
+
throw new Error("The pass's NAVI account does not match the reviewed lending rules");
|
|
90
|
+
}
|
|
91
|
+
/** The allowances and revoke caps the authorization created, grouped by coin. */
|
|
92
|
+
function allowanceObjects(objects, owner) {
|
|
93
|
+
const allowances = {}, caps = [];
|
|
94
|
+
for (const o of objects) {
|
|
95
|
+
const coin = allowanceCoin(o.type);
|
|
96
|
+
if (coin && o.shared)
|
|
97
|
+
(allowances[coin] ??= []).push(o.id);
|
|
98
|
+
const tag = parseStructTag(o.type);
|
|
99
|
+
if (tag.address === normalizeSuiAddress('0x2') &&
|
|
100
|
+
tag.module === 'allowance' &&
|
|
101
|
+
tag.name === 'AllowanceCap' &&
|
|
102
|
+
!o.shared &&
|
|
103
|
+
o.owner === owner)
|
|
104
|
+
caps.push(o.id);
|
|
105
|
+
}
|
|
106
|
+
return { allowances, caps };
|
|
107
|
+
}
|
|
108
|
+
function checkAllowance(a, expected) {
|
|
109
|
+
if (!a)
|
|
110
|
+
throw new Error('Allowance is not on chain');
|
|
111
|
+
const ok = a.funder === expected.funder &&
|
|
112
|
+
a.spender === expected.spender &&
|
|
113
|
+
a.app === (expected.app && normalizeStructTag(expected.app)) &&
|
|
114
|
+
a.name === expected.name &&
|
|
115
|
+
a.lifetimeCap === expected.lifetimeCap &&
|
|
116
|
+
a.expirationMs === String(expected.expirationMs) &&
|
|
117
|
+
a.startMs === null &&
|
|
118
|
+
a.currentSpend === '0' &&
|
|
119
|
+
(expected.dailyLimit === null
|
|
120
|
+
? a.rateLimit === null
|
|
121
|
+
: a.rateLimit?.limit === expected.dailyLimit && a.rateLimit.periodMs === '86400000');
|
|
122
|
+
if (!ok || Number(a.expirationMs) <= a.chainTimeMs)
|
|
123
|
+
throw new Error(`On-chain allowance for ${a.coinType} does not match the reviewed setup`);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Everything the authorization transaction must have created, checked against the setup: one
|
|
127
|
+
* pass-bound allowance per reviewed coin, the gas allowance, a revoke cap for each, nothing
|
|
128
|
+
* more, the receipt in the owner's wallet, and the control with exactly the reviewed policies.
|
|
129
|
+
*/
|
|
130
|
+
async function verifySession(store, tx, owner, digest) {
|
|
131
|
+
const { deployment: d, setup, sessionAddress } = store.state, client = clientFor(d);
|
|
132
|
+
const raw = allowanceObjects(tx.objects, owner);
|
|
133
|
+
const views = Object.fromEntries(await Promise.all(Object.entries(raw.allowances).map(async ([coin, ids]) => [
|
|
134
|
+
coin,
|
|
135
|
+
await Promise.all(ids.map((id) => readAllowance(client, id, coin))),
|
|
136
|
+
])));
|
|
137
|
+
const capTargets = await Promise.all(raw.caps.map((id) => readAllowanceCapTarget(client, id)));
|
|
138
|
+
const capFor = (allowanceId) => {
|
|
139
|
+
const i = capTargets.findIndex((t) => t === allowanceId);
|
|
140
|
+
if (i < 0)
|
|
141
|
+
throw new Error(`No revoke cap found for allowance ${allowanceId}`);
|
|
142
|
+
return raw.caps[i];
|
|
143
|
+
};
|
|
144
|
+
const common = { funder: owner, spender: sessionAddress, expirationMs: setup.expiresMs };
|
|
145
|
+
const app = normalizeStructTag(appType(d));
|
|
146
|
+
// The gas allowance is the SUI one with no app binding and its fixed name.
|
|
147
|
+
const gasView = (views[SUI] ?? []).find((v) => v?.name === AGENT_GAS_NAME && v.app === null);
|
|
148
|
+
if (!gasView)
|
|
149
|
+
throw new Error('The authorization created no gas allowance');
|
|
150
|
+
checkAllowance(gasView, {
|
|
151
|
+
...common,
|
|
152
|
+
app: null,
|
|
153
|
+
name: AGENT_GAS_NAME,
|
|
154
|
+
lifetimeCap: setup.gasCap,
|
|
155
|
+
dailyLimit: setup.gasDaily,
|
|
156
|
+
});
|
|
157
|
+
const allowances = {};
|
|
158
|
+
for (const [coin, cap] of Object.entries(setup.allowances)) {
|
|
159
|
+
const bound = (views[normalizeStructTag(coin)] ?? []).filter((v) => v?.app === app);
|
|
160
|
+
if (bound.length !== 1 || !bound[0])
|
|
161
|
+
throw new Error(`Expected exactly one pass-bound ${coin} allowance for this owner`);
|
|
162
|
+
checkAllowance(bound[0], {
|
|
163
|
+
...common,
|
|
164
|
+
app: appType(d),
|
|
165
|
+
name: setup.name,
|
|
166
|
+
dailyLimit: null,
|
|
167
|
+
lifetimeCap: cap,
|
|
168
|
+
});
|
|
169
|
+
allowances[normalizeStructTag(coin)] = {
|
|
170
|
+
allowance: bound[0].id,
|
|
171
|
+
revokeCap: capFor(bound[0].id),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const expectedCount = Object.keys(setup.allowances).length + 1;
|
|
175
|
+
if (Object.values(views).flat().length !== expectedCount || raw.caps.length !== expectedCount)
|
|
176
|
+
throw new Error('Authorization created an allowance the reviewed setup did not include');
|
|
177
|
+
const ids = { allowances, gasAllowance: gasView.id, gasRevokeCap: capFor(gasView.id) };
|
|
178
|
+
const created = tx.passes.filter((c) => c.owner === owner && c.agent === sessionAddress);
|
|
179
|
+
if (created.length !== 1)
|
|
180
|
+
throw new Error('Expected one new pass for this session in the transaction');
|
|
181
|
+
const { id: passId, receiptId } = created[0];
|
|
182
|
+
const issued = tx.issued.filter((i) => i.passId === passId);
|
|
183
|
+
const issuedFor = (coin, cap) => issued.some((i) => i.coinType === normalizeStructTag(coin) &&
|
|
184
|
+
i.lifetimeCap === cap &&
|
|
185
|
+
i.expiresMs === String(setup.expiresMs));
|
|
186
|
+
if (issued.length !== Object.keys(setup.allowances).length ||
|
|
187
|
+
!Object.entries(setup.allowances).every(([coin, cap]) => issuedFor(coin, cap)))
|
|
188
|
+
throw new Error('Issued allowances do not match the reviewed setup');
|
|
189
|
+
// The receipt the pass named must exist, be ours, and sit in the owner's wallet.
|
|
190
|
+
const receipt = tx.objects.find((o) => o.id === receiptId);
|
|
191
|
+
if (!receipt || receipt.type !== normalizeStructTag(receiptType(d)) || receipt.owner !== owner)
|
|
192
|
+
throw new Error('The pass receipt did not reach the owner');
|
|
193
|
+
const control = await readControl(client, d, passId);
|
|
194
|
+
checkControl(control, d, setup, owner, sessionAddress);
|
|
195
|
+
return { passId, receiptId, owner, ...ids, authorizationDigest: digest };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Whether the chain now holds the rules a session proposed for its pass. Nothing is sent back
|
|
199
|
+
* from the page, so this reads the pass, its policies and the owner's allowances as they are,
|
|
200
|
+
* and once they all equal the pending setup the session adopts it: new rules, and the allowance
|
|
201
|
+
* ids now bound to the pass. Lifetime spend on a kept allowance is whatever it was.
|
|
202
|
+
*/
|
|
203
|
+
export async function applyPendingChange(store) {
|
|
204
|
+
const { deployment: d, pendingChange: next, active, sessionAddress } = store.state;
|
|
205
|
+
if (!next || !active)
|
|
206
|
+
return false;
|
|
207
|
+
const client = clientFor(d);
|
|
208
|
+
const control = await readControl(client, d, active.passId);
|
|
209
|
+
if (control.owner !== active.owner ||
|
|
210
|
+
control.agent !== sessionAddress ||
|
|
211
|
+
control.agentExpiresMs !== String(next.expiresMs) ||
|
|
212
|
+
!isDeepStrictEqual(rulesOnly(control.policies), expectedPolicies(d, next.policy)) ||
|
|
213
|
+
(next.policy.lending.length > 0 && !control.naviPosition))
|
|
214
|
+
return false;
|
|
215
|
+
const found = await discoverAllowances(client, d, { owner: active.owner, agent: sessionAddress });
|
|
216
|
+
if (!found.gas)
|
|
217
|
+
return false;
|
|
218
|
+
const caps = Object.fromEntries(Object.entries(next.allowances).map(([c, cap]) => [normalizeStructTag(c), cap]));
|
|
219
|
+
const want = Object.keys(caps);
|
|
220
|
+
if (want.length !== Object.keys(found.allowances).length ||
|
|
221
|
+
!want.every((c) => c in found.allowances))
|
|
222
|
+
return false;
|
|
223
|
+
const fits = (v, cap, daily) => !!v &&
|
|
224
|
+
v.funder === active.owner &&
|
|
225
|
+
v.spender === sessionAddress &&
|
|
226
|
+
v.lifetimeCap === cap &&
|
|
227
|
+
v.expirationMs === String(next.expiresMs) &&
|
|
228
|
+
(daily === null ? v.rateLimit === null : v.rateLimit?.limit === daily);
|
|
229
|
+
for (const coin of want) {
|
|
230
|
+
const v = await readAllowance(client, found.allowances[coin].allowance, coin);
|
|
231
|
+
if (!fits(v, next.allowances[coin] ??
|
|
232
|
+
next.allowances[Object.keys(next.allowances).find((c) => normalizeStructTag(c) === coin)], null) ||
|
|
233
|
+
v.app !== normalizeStructTag(appType(d)))
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const gas = await readAllowance(client, found.gas.allowance, SUI);
|
|
237
|
+
if (!fits(gas, next.gasCap, next.gasDaily) || gas.app !== null)
|
|
238
|
+
return false;
|
|
239
|
+
await store.applyChange(next, {
|
|
240
|
+
...active,
|
|
241
|
+
allowances: found.allowances,
|
|
242
|
+
gasAllowance: found.gas.allowance,
|
|
243
|
+
gasRevokeCap: found.gas.revokeCap,
|
|
244
|
+
});
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
export async function activate(store, digest, ownerInput) {
|
|
248
|
+
const { deployment: d, setup, sessionAddress } = store.state;
|
|
249
|
+
const owner = normalizeSuiAddress(ownerInput), client = clientFor(d);
|
|
250
|
+
await assertChain(client, d);
|
|
251
|
+
const tx = await readAuthorization(client, d, digest);
|
|
252
|
+
if (tx.sender !== owner)
|
|
253
|
+
throw new Error('Authorization sender does not match connected wallet');
|
|
254
|
+
const active = await verifySession(store, tx, owner, digest);
|
|
255
|
+
assertOnlyReviewedGrants(tx.objects, active);
|
|
256
|
+
assertExactStipend(tx.balanceChanges, sessionAddress, SUI, setup.gasAmount);
|
|
257
|
+
await store.activate(active);
|
|
258
|
+
return active;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The review page builds the PTB and the wallet signs whatever it was handed, so the transaction
|
|
262
|
+
* may carry grants the owner never reviewed. Refuse any allowance or revoke cap this session does
|
|
263
|
+
* not account for.
|
|
264
|
+
*/
|
|
265
|
+
function assertOnlyReviewedGrants(objects, active) {
|
|
266
|
+
const reviewed = new Set([
|
|
267
|
+
...Object.values(active.allowances).flatMap((a) => [a.allowance, a.revokeCap]),
|
|
268
|
+
active.gasAllowance,
|
|
269
|
+
active.gasRevokeCap,
|
|
270
|
+
]);
|
|
271
|
+
for (const o of objects) {
|
|
272
|
+
const tag = parseStructTag(o.type);
|
|
273
|
+
const isGrant = tag.address === normalizeSuiAddress('0x2') &&
|
|
274
|
+
tag.module === 'allowance' &&
|
|
275
|
+
(tag.name === 'Allowance' || tag.name === 'AllowanceCap');
|
|
276
|
+
if (isGrant && !reviewed.has(o.id))
|
|
277
|
+
throw new Error('Authorization created an allowance the reviewed setup did not include');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** The session must receive exactly the reviewed gas stipend in SUI and nothing else. */
|
|
281
|
+
function assertExactStipend(changes, session, sui, gasAmount) {
|
|
282
|
+
const credits = changes.filter((c) => c.address === session);
|
|
283
|
+
if (credits.length !== 1 ||
|
|
284
|
+
credits[0].coinType !== normalizeStructTag(sui) ||
|
|
285
|
+
BigInt(credits[0].amount) !== BigInt(gasAmount))
|
|
286
|
+
throw new Error('Session address must receive exactly the reviewed gas stipend and nothing else');
|
|
287
|
+
}
|