@ar.io/sdk 3.24.0 → 4.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +682 -600
- package/lib/esm/cli/cli.js +188 -152
- package/lib/esm/cli/commands/antCommands.js +23 -58
- package/lib/esm/cli/commands/arnsPurchaseCommands.js +48 -30
- package/lib/esm/cli/commands/escrowCommands.js +221 -0
- package/lib/esm/cli/commands/gatewayWriteCommands.js +142 -23
- package/lib/esm/cli/commands/pruneCommands.js +150 -0
- package/lib/esm/cli/commands/readCommands.js +22 -3
- package/lib/esm/cli/commands/transfer.js +6 -6
- package/lib/esm/cli/options.js +124 -58
- package/lib/esm/cli/utils.js +280 -174
- package/lib/esm/common/ant-registry.js +17 -143
- package/lib/esm/common/ant.js +44 -1167
- package/lib/esm/common/faucet.js +11 -6
- package/lib/esm/common/index.js +0 -4
- package/lib/esm/common/io.js +25 -1412
- package/lib/esm/constants.js +13 -19
- package/lib/esm/solana/ant-readable.js +724 -0
- package/lib/esm/solana/ant-registry-readable.js +133 -0
- package/lib/esm/solana/ant-registry-writeable.js +472 -0
- package/lib/esm/solana/ant-writeable.js +384 -0
- package/lib/esm/solana/ata.js +70 -0
- package/lib/esm/solana/canonical-message.js +128 -0
- package/lib/esm/solana/clusters.js +111 -0
- package/lib/esm/solana/constants.js +146 -0
- package/lib/esm/solana/delegation-math.js +112 -0
- package/lib/esm/solana/deserialize.js +711 -0
- package/lib/esm/solana/escrow.js +839 -0
- package/lib/{cjs/utils/json.js → esm/solana/events.js} +15 -10
- package/lib/esm/solana/funding-plan.js +699 -0
- package/lib/esm/solana/index.js +126 -0
- package/lib/esm/solana/instruction.js +39 -0
- package/lib/esm/solana/io-readable.js +2182 -0
- package/lib/esm/solana/io-writeable.js +3196 -0
- package/lib/esm/solana/json-rpc.js +90 -0
- package/lib/esm/solana/metadata.js +81 -0
- package/lib/esm/solana/mpl-core.js +192 -0
- package/lib/esm/solana/pda.js +332 -0
- package/lib/esm/solana/predict-prescribed-observers.js +110 -0
- package/lib/esm/solana/retry.js +117 -0
- package/lib/esm/solana/rpc-circuit-breaker.js +258 -0
- package/lib/esm/solana/send.js +372 -0
- package/lib/esm/solana/spawn-ant.js +224 -0
- package/lib/esm/solana/types.js +1 -0
- package/lib/esm/types/ant.js +27 -15
- package/lib/esm/types/io.js +8 -11
- package/lib/esm/utils/ant.js +0 -63
- package/lib/esm/utils/index.js +0 -3
- package/lib/esm/version.js +1 -1
- package/lib/types/cli/commands/antCommands.d.ts +5 -13
- package/lib/types/cli/commands/arnsPurchaseCommands.d.ts +33 -7
- package/lib/types/cli/commands/escrowCommands.d.ts +68 -0
- package/lib/types/cli/commands/gatewayWriteCommands.d.ts +12 -11
- package/lib/types/cli/commands/pruneCommands.d.ts +31 -0
- package/lib/types/cli/commands/readCommands.d.ts +27 -22
- package/lib/types/cli/commands/transfer.d.ts +9 -9
- package/lib/types/cli/options.d.ts +76 -21
- package/lib/types/cli/types.d.ts +11 -13
- package/lib/types/cli/utils.d.ts +71 -31
- package/lib/types/common/ant-registry.d.ts +49 -47
- package/lib/types/common/ant.d.ts +54 -539
- package/lib/types/common/faucet.d.ts +20 -8
- package/lib/types/common/index.d.ts +0 -3
- package/lib/types/common/io.d.ts +51 -263
- package/lib/types/constants.d.ts +11 -18
- package/lib/types/solana/ant-readable.d.ts +180 -0
- package/lib/types/solana/ant-registry-readable.d.ts +105 -0
- package/lib/types/solana/ant-registry-writeable.d.ts +249 -0
- package/lib/types/solana/ant-writeable.d.ts +177 -0
- package/lib/types/solana/ata.d.ts +44 -0
- package/lib/types/solana/canonical-message.d.ts +121 -0
- package/lib/types/solana/clusters.d.ts +109 -0
- package/lib/types/solana/constants.d.ts +119 -0
- package/lib/types/solana/delegation-math.d.ts +45 -0
- package/lib/types/solana/deserialize.d.ts +262 -0
- package/lib/types/solana/escrow.d.ts +480 -0
- package/lib/types/solana/events.d.ts +38 -0
- package/lib/types/solana/funding-plan.d.ts +225 -0
- package/lib/types/solana/index.d.ts +87 -0
- package/lib/types/solana/instruction.d.ts +39 -0
- package/lib/types/solana/io-readable.d.ts +499 -0
- package/lib/types/solana/io-writeable.d.ts +893 -0
- package/lib/types/solana/json-rpc.d.ts +47 -0
- package/lib/types/solana/metadata.d.ts +84 -0
- package/lib/types/solana/mpl-core.d.ts +120 -0
- package/lib/types/solana/pda.d.ts +95 -0
- package/lib/types/solana/predict-prescribed-observers.d.ts +28 -0
- package/lib/types/solana/retry.d.ts +62 -0
- package/lib/types/solana/rpc-circuit-breaker.d.ts +78 -0
- package/lib/types/solana/send.d.ts +94 -0
- package/lib/types/solana/spawn-ant.d.ts +145 -0
- package/lib/types/solana/types.d.ts +82 -0
- package/lib/types/types/ant-registry.d.ts +43 -4
- package/lib/types/types/ant.d.ts +114 -96
- package/lib/types/types/common.d.ts +18 -74
- package/lib/types/types/faucet.d.ts +2 -2
- package/lib/types/types/io.d.ts +244 -158
- package/lib/types/types/token.d.ts +0 -12
- package/lib/types/utils/ant.d.ts +1 -12
- package/lib/types/utils/index.d.ts +0 -3
- package/lib/types/version.d.ts +1 -1
- package/package.json +36 -33
- package/lib/cjs/cli/cli.js +0 -822
- package/lib/cjs/cli/commands/antCommands.js +0 -113
- package/lib/cjs/cli/commands/arnsPurchaseCommands.js +0 -212
- package/lib/cjs/cli/commands/gatewayWriteCommands.js +0 -210
- package/lib/cjs/cli/commands/readCommands.js +0 -215
- package/lib/cjs/cli/commands/transfer.js +0 -159
- package/lib/cjs/cli/options.js +0 -470
- package/lib/cjs/cli/types.js +0 -2
- package/lib/cjs/cli/utils.js +0 -639
- package/lib/cjs/common/ant-registry.js +0 -155
- package/lib/cjs/common/ant-versions.js +0 -93
- package/lib/cjs/common/ant.js +0 -1182
- package/lib/cjs/common/arweave.js +0 -27
- package/lib/cjs/common/contracts/ao-process.js +0 -224
- package/lib/cjs/common/error.js +0 -64
- package/lib/cjs/common/faucet.js +0 -150
- package/lib/cjs/common/hyperbeam/hb.js +0 -173
- package/lib/cjs/common/index.js +0 -42
- package/lib/cjs/common/io.js +0 -1423
- package/lib/cjs/common/logger.js +0 -83
- package/lib/cjs/common/loggers/winston.js +0 -68
- package/lib/cjs/common/marketplace.js +0 -731
- package/lib/cjs/common/turbo.js +0 -223
- package/lib/cjs/constants.js +0 -41
- package/lib/cjs/node/index.js +0 -39
- package/lib/cjs/package.json +0 -1
- package/lib/cjs/types/ant-registry.js +0 -2
- package/lib/cjs/types/ant.js +0 -168
- package/lib/cjs/types/common.js +0 -2
- package/lib/cjs/types/faucet.js +0 -2
- package/lib/cjs/types/index.js +0 -37
- package/lib/cjs/types/io.js +0 -51
- package/lib/cjs/types/token.js +0 -116
- package/lib/cjs/utils/ant.js +0 -108
- package/lib/cjs/utils/ao.js +0 -432
- package/lib/cjs/utils/arweave.js +0 -285
- package/lib/cjs/utils/base64.js +0 -62
- package/lib/cjs/utils/hash.js +0 -56
- package/lib/cjs/utils/index.js +0 -38
- package/lib/cjs/utils/processes.js +0 -173
- package/lib/cjs/utils/random.js +0 -30
- package/lib/cjs/utils/schema.js +0 -15
- package/lib/cjs/utils/url.js +0 -37
- package/lib/cjs/version.js +0 -20
- package/lib/cjs/web/index.js +0 -41
- package/lib/esm/common/ant-versions.js +0 -87
- package/lib/esm/common/arweave.js +0 -21
- package/lib/esm/common/contracts/ao-process.js +0 -220
- package/lib/esm/common/hyperbeam/hb.js +0 -169
- package/lib/esm/common/marketplace.js +0 -724
- package/lib/esm/common/turbo.js +0 -215
- package/lib/esm/node/index.js +0 -20
- package/lib/esm/utils/ao.js +0 -420
- package/lib/esm/utils/arweave.js +0 -271
- package/lib/esm/utils/processes.js +0 -167
- package/lib/esm/web/index.js +0 -20
- package/lib/types/common/ant-versions.d.ts +0 -39
- package/lib/types/common/arweave.d.ts +0 -17
- package/lib/types/common/contracts/ao-process.d.ts +0 -47
- package/lib/types/common/hyperbeam/hb.d.ts +0 -88
- package/lib/types/common/marketplace.d.ts +0 -568
- package/lib/types/common/turbo.d.ts +0 -61
- package/lib/types/node/index.d.ts +0 -20
- package/lib/types/utils/ao.d.ts +0 -80
- package/lib/types/utils/arweave.d.ts +0 -79
- package/lib/types/utils/processes.d.ts +0 -39
- package/lib/types/web/index.d.ts +0 -20
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
18
|
+
*
|
|
19
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
20
|
+
* you may not use this file except in compliance with the License.
|
|
21
|
+
* You may obtain a copy of the License at
|
|
22
|
+
*
|
|
23
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Solana implementation of the ANT Registry read interface.
|
|
27
|
+
*
|
|
28
|
+
* Backed by the per-user paginated ACL (ADR-012): a head `AclConfig` PDA
|
|
29
|
+
* and N content-addressable `AclPage` PDAs, each holding up to
|
|
30
|
+
* `MAX_ACL_PAGE_ENTRIES` `(asset, role)` tuples. Frontends can fetch a
|
|
31
|
+
* user's ANTs in two RPC calls — one `getAccountInfo` for `AclConfig` plus
|
|
32
|
+
* one `getMultipleAccountsInfo` for every page — instead of a
|
|
33
|
+
* `getProgramAccounts` scan, a DAS provider, or a foundation-hosted
|
|
34
|
+
* indexer.
|
|
35
|
+
*
|
|
36
|
+
* Usage:
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { createSolanaRpc } from '@solana/kit';
|
|
39
|
+
* import { ANTRegistry } from '@ar.io/sdk';
|
|
40
|
+
*
|
|
41
|
+
* const registry = ANTRegistry.init({
|
|
42
|
+
* backend: 'solana',
|
|
43
|
+
* rpc: createSolanaRpc('https://api.mainnet-beta.solana.com'),
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* const { Owned, Controlled } = await registry.accessControlList({
|
|
47
|
+
* address: 'SomeSolanaWalletAddress...',
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* When a user has no on-chain `AclConfig` (never registered / not yet
|
|
52
|
+
* populated), both lists return empty. The write path (SDK ANT write
|
|
53
|
+
* methods + migration tooling) is responsible for keeping the ACL in sync
|
|
54
|
+
* as owners / controllers change.
|
|
55
|
+
*/
|
|
56
|
+
import { address } from '@solana/kit';
|
|
57
|
+
import { Logger } from '../common/logger.js';
|
|
58
|
+
import { ACL_ROLE_CONTROLLER, ACL_ROLE_OWNER, ARIO_ANT_PROGRAM_ID, } from './constants.js';
|
|
59
|
+
import { deserializeAclConfig, deserializeAclPage } from './deserialize.js';
|
|
60
|
+
import { getAccountInfoLegacy, getMultipleAccountsInfoLegacy, } from './json-rpc.js';
|
|
61
|
+
import { getAclConfigPDA, getAclPagePDA } from './pda.js';
|
|
62
|
+
export class SolanaANTRegistryReadable {
|
|
63
|
+
rpc;
|
|
64
|
+
commitment;
|
|
65
|
+
/** Deployed `ario-ant` program id this registry talks to. */
|
|
66
|
+
antProgram;
|
|
67
|
+
logger;
|
|
68
|
+
constructor(config) {
|
|
69
|
+
this.rpc = config.rpc;
|
|
70
|
+
this.commitment = config.commitment ?? 'confirmed';
|
|
71
|
+
this.antProgram = config.antProgramId ?? ARIO_ANT_PROGRAM_ID;
|
|
72
|
+
this.logger = config.logger ?? Logger.default;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read a user's `AclConfig` head plus every `AclPage` and return owned +
|
|
76
|
+
* controlled ANT mint lists. Returns empty lists if the head PDA does not
|
|
77
|
+
* exist yet.
|
|
78
|
+
*
|
|
79
|
+
* **Note:** This is an eventually-consistent secondary index, not a
|
|
80
|
+
* canonical source of truth. Marketplace transfers update NFT ownership
|
|
81
|
+
* on-chain immediately but the ACL is only updated when someone calls
|
|
82
|
+
* `record_acl_owner` / `remove_acl_owner`. For real-time accuracy on a
|
|
83
|
+
* specific ANT, check the Metaplex Core asset owner directly.
|
|
84
|
+
*/
|
|
85
|
+
async accessControlList({ address: addr, }) {
|
|
86
|
+
const userAddr = address(addr);
|
|
87
|
+
const [configPda] = await getAclConfigPDA(userAddr, this.antProgram);
|
|
88
|
+
this.logger.debug?.('Fetching AclConfig PDA', {
|
|
89
|
+
address: addr,
|
|
90
|
+
pda: configPda,
|
|
91
|
+
});
|
|
92
|
+
const configAccount = await getAccountInfoLegacy(this.rpc, configPda, this.commitment);
|
|
93
|
+
if (!configAccount) {
|
|
94
|
+
this.logger.debug?.('AclConfig not found — returning empty lists', {
|
|
95
|
+
address: addr,
|
|
96
|
+
});
|
|
97
|
+
return { Owned: [], Controlled: [] };
|
|
98
|
+
}
|
|
99
|
+
const config = deserializeAclConfig(configAccount.data);
|
|
100
|
+
if (config.pageCount === 0n) {
|
|
101
|
+
return { Owned: [], Controlled: [] };
|
|
102
|
+
}
|
|
103
|
+
// Derive every page PDA up-front, then load them in a single
|
|
104
|
+
// `getMultipleAccountsInfo` round trip.
|
|
105
|
+
const pageAddrs = [];
|
|
106
|
+
for (let i = 0n; i < config.pageCount; i++) {
|
|
107
|
+
const [pagePda] = await getAclPagePDA(userAddr, i, this.antProgram);
|
|
108
|
+
pageAddrs.push(pagePda);
|
|
109
|
+
}
|
|
110
|
+
const pageAccounts = await getMultipleAccountsInfoLegacy(this.rpc, pageAddrs, this.commitment);
|
|
111
|
+
const owned = [];
|
|
112
|
+
const controlled = [];
|
|
113
|
+
for (const acct of pageAccounts) {
|
|
114
|
+
if (!acct)
|
|
115
|
+
continue;
|
|
116
|
+
const page = deserializeAclPage(acct.data);
|
|
117
|
+
for (const entry of page.entries) {
|
|
118
|
+
if (entry.role === ACL_ROLE_OWNER)
|
|
119
|
+
owned.push(entry.asset);
|
|
120
|
+
else if (entry.role === ACL_ROLE_CONTROLLER)
|
|
121
|
+
controlled.push(entry.asset);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { Owned: owned, Controlled: controlled };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Cleaner alias for `accessControlList` — matches the AO backend so
|
|
128
|
+
* consumers can switch backends without renaming calls.
|
|
129
|
+
*/
|
|
130
|
+
async getAntsForAddress({ address, }) {
|
|
131
|
+
return this.accessControlList({ address });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2022-2024 Permanent Data Solutions, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Solana implementation of the ANT Registry write interface.
|
|
18
|
+
*
|
|
19
|
+
* This class owns the per-user paginated ACL surface (ADR-012). After
|
|
20
|
+
* the on-chain hardening pass, the *primary* contract handlers
|
|
21
|
+
* (`add_controller`, `remove_controller`, `transfer`) write the ACL
|
|
22
|
+
* inline as part of their own instruction — Codama renders the ACL
|
|
23
|
+
* accounts as required, so callers cannot bypass them. The registry's
|
|
24
|
+
* job is therefore split in two:
|
|
25
|
+
*
|
|
26
|
+
* 1. **Preflight resolution.** Pick the right `AclConfig` + `AclPage`
|
|
27
|
+
* to pass for a record / remove operation, and emit the
|
|
28
|
+
* `register_acl_config` / `add_acl_page` ixs needed to bootstrap
|
|
29
|
+
* missing accounts. See `resolveDestinationAclAccounts` and
|
|
30
|
+
* `resolveSourceAclAccountsForEntry`.
|
|
31
|
+
* 2. **Bulk maintenance.** For operations the contract can't bundle
|
|
32
|
+
* atomically (notably the variable-length ex-controller cleanup
|
|
33
|
+
* after a transfer, plus any caller-driven heal flows), expose
|
|
34
|
+
* `planAclMaintenance` + the standalone `record_acl_*` /
|
|
35
|
+
* `remove_acl_*` instruction builders. `SolanaANTWriteable`
|
|
36
|
+
* forwards an `aclOps` array to `sendTransaction` for these.
|
|
37
|
+
*
|
|
38
|
+
* Why the registry, not `SolanaANTWriteable`?
|
|
39
|
+
* The ACL is a per-user structure shared across every ANT a user
|
|
40
|
+
* touches, so the right home for the page-selection / preflight logic
|
|
41
|
+
* is the registry itself. `SolanaANTWriteable` composes a
|
|
42
|
+
* `SolanaANTRegistryWriteable` and delegates ACL planning to it.
|
|
43
|
+
*
|
|
44
|
+
* Per-user ACL layout (recap):
|
|
45
|
+
* - `AclConfig` head at `["acl_config", user]` tracks `page_count` +
|
|
46
|
+
* `total_entries`.
|
|
47
|
+
* - Each `AclPage` is a content-addressable PDA at
|
|
48
|
+
* `["acl_page", user, page_idx_le]` holding up to
|
|
49
|
+
* `MAX_ACL_PAGE_ENTRIES` `(asset, role)` tuples.
|
|
50
|
+
*
|
|
51
|
+
* NOTE: `register_acl_config` and `add_acl_page` are permissionless —
|
|
52
|
+
* anyone can pay rent on behalf of any user. `payer` (the writeable
|
|
53
|
+
* registry's signer) acts as the rent payer for everything bundled here.
|
|
54
|
+
*/
|
|
55
|
+
import { address, } from '@solana/kit';
|
|
56
|
+
import { getAddAclPageInstruction, getCloseAclConfigInstruction, getCloseAclPageInstruction, getRecordAclControllerInstructionAsync, getRecordAclOwnerInstructionAsync, getRegisterAclConfigInstruction, getRemoveAclControllerInstructionAsync, getRemoveAclOwnerInstructionAsync, } from '@ar.io/solana-contracts/ant';
|
|
57
|
+
import { SolanaANTRegistryReadable, } from './ant-registry-readable.js';
|
|
58
|
+
import { ACL_ROLE_CONTROLLER, ACL_ROLE_OWNER, MAX_ACL_PAGE_ENTRIES, } from './constants.js';
|
|
59
|
+
import { deserializeAclConfig, deserializeAclPage } from './deserialize.js';
|
|
60
|
+
import { getAccountInfoLegacy, getMultipleAccountsInfoLegacy, } from './json-rpc.js';
|
|
61
|
+
import { getAclConfigPDA, getAclPagePDA } from './pda.js';
|
|
62
|
+
/** Map the cross-backend role string → on-chain `u8` byte. */
|
|
63
|
+
const ROLE_TO_BYTE = {
|
|
64
|
+
owner: ACL_ROLE_OWNER,
|
|
65
|
+
controller: ACL_ROLE_CONTROLLER,
|
|
66
|
+
};
|
|
67
|
+
export class SolanaANTRegistryWriteable extends SolanaANTRegistryReadable {
|
|
68
|
+
signer;
|
|
69
|
+
constructor(config) {
|
|
70
|
+
super(config);
|
|
71
|
+
this.signer = config.signer;
|
|
72
|
+
}
|
|
73
|
+
// =========================================
|
|
74
|
+
// Cross-backend `register` no-op (Solana populates the ACL lazily)
|
|
75
|
+
// =========================================
|
|
76
|
+
/**
|
|
77
|
+
* The Solana ANT registry does not have a centralised "register" step —
|
|
78
|
+
* `AclConfig` is created lazily the first time a user becomes an owner
|
|
79
|
+
* or controller (via `register_acl_config`, which `planAclMaintenance`
|
|
80
|
+
* emits automatically). This method exists only to satisfy the
|
|
81
|
+
* cross-backend `ANTRegistryWrite` interface.
|
|
82
|
+
*/
|
|
83
|
+
async register(_params) {
|
|
84
|
+
return { id: '' };
|
|
85
|
+
}
|
|
86
|
+
// =========================================
|
|
87
|
+
// Preflight: resolve ACL accounts for inline contract handlers
|
|
88
|
+
// =========================================
|
|
89
|
+
/**
|
|
90
|
+
* Pure PDA derivation — no RPC. Useful for callers that already
|
|
91
|
+
* resolved the page via `resolveSourceAclAccountsForEntry` and need a
|
|
92
|
+
* fallback PDA (e.g. `removeController` when the entry can't be found
|
|
93
|
+
* but we still need to pass *some* page address to the contract so
|
|
94
|
+
* the on-chain handler returns the right error).
|
|
95
|
+
*/
|
|
96
|
+
async deriveAclConfigPda(user) {
|
|
97
|
+
const [pda] = await getAclConfigPDA(user, this.antProgram);
|
|
98
|
+
return pda;
|
|
99
|
+
}
|
|
100
|
+
/** Pure PDA derivation — see {@link deriveAclConfigPda}. */
|
|
101
|
+
async deriveAclPagePda(user, pageIdx) {
|
|
102
|
+
const [pda] = await getAclPagePDA(user, pageIdx, this.antProgram);
|
|
103
|
+
return pda;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Pick the `AclConfig` + destination `AclPage` to wire into a contract
|
|
107
|
+
* handler that is about to **record** an entry for `user` (e.g.
|
|
108
|
+
* `ario-ant::add_controller`, `ario-ant::transfer`'s new-owner side).
|
|
109
|
+
*
|
|
110
|
+
* Behaviour mirrors the planner's record path:
|
|
111
|
+
* - If `AclConfig(user)` does not exist yet, emit
|
|
112
|
+
* `register_acl_config` so the on-chain handler's seed-binding
|
|
113
|
+
* resolves.
|
|
114
|
+
* - If every existing `AclPage` is at `MAX_ACL_PAGE_ENTRIES`, emit
|
|
115
|
+
* `add_acl_page` for `page_idx == page_count` and use it as the
|
|
116
|
+
* destination.
|
|
117
|
+
* - Otherwise pick the **first non-full page** so density recovers
|
|
118
|
+
* after a `swap_remove` left a mid-life page sparse (see
|
|
119
|
+
* `docs/ACCOUNT_SCALING_PATTERNS.md` § Pattern C).
|
|
120
|
+
*
|
|
121
|
+
* Returns the resolved PDAs plus any prep ixs the caller must
|
|
122
|
+
* **prepend** to the bundle. Idempotent: safe to call again on a
|
|
123
|
+
* partially-bootstrapped ACL — only the missing prep ixs come back.
|
|
124
|
+
*
|
|
125
|
+
* The on-chain handler still validates `acl_config.user == user` and
|
|
126
|
+
* the page seed binding, so a stale resolution simply fails the tx;
|
|
127
|
+
* over-emission is bounded by the current page layout.
|
|
128
|
+
*/
|
|
129
|
+
async resolveDestinationAclAccounts(params) {
|
|
130
|
+
const state = await this.loadAclState(params.user);
|
|
131
|
+
const prepIxs = [];
|
|
132
|
+
if (!state.exists) {
|
|
133
|
+
prepIxs.push(await this.buildRegisterAclConfigIx({ user: params.user }));
|
|
134
|
+
state.exists = true;
|
|
135
|
+
}
|
|
136
|
+
let pageIdx = findPageWithRoom(state);
|
|
137
|
+
if (pageIdx === null) {
|
|
138
|
+
pageIdx = state.pageCount;
|
|
139
|
+
prepIxs.push(await this.buildAddAclPageIx({ user: params.user, pageIdx }));
|
|
140
|
+
}
|
|
141
|
+
const [aclConfigPda] = await getAclConfigPDA(params.user, this.antProgram);
|
|
142
|
+
const [aclPagePda] = await getAclPagePDA(params.user, pageIdx, this.antProgram);
|
|
143
|
+
return { aclConfigPda, aclPagePda, pageIdx, prepIxs };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Locate the `AclPage` that currently holds `(asset, role)` for
|
|
147
|
+
* `user`, so a contract handler that is about to **remove** the entry
|
|
148
|
+
* (`ario-ant::remove_controller`, `ario-ant::transfer`'s old-owner
|
|
149
|
+
* side) can be wired with the right page accounts.
|
|
150
|
+
*
|
|
151
|
+
* Returns `null` if `user` has no `AclConfig` head or no matching
|
|
152
|
+
* entry — callers that want strict semantics should treat that as "no
|
|
153
|
+
* record to remove" and either skip the wrapped ix entirely (when the
|
|
154
|
+
* primary mutation is also unnecessary) or fall back to the standalone
|
|
155
|
+
* `remove_acl_*` heal flow.
|
|
156
|
+
*
|
|
157
|
+
* The on-chain handler does the strict check via `position_of` on
|
|
158
|
+
* the supplied page, so a wrong / stale resolution fails the tx with
|
|
159
|
+
* `AclEntryNotFound` rather than corrupting state.
|
|
160
|
+
*/
|
|
161
|
+
async resolveSourceAclAccountsForEntry(params) {
|
|
162
|
+
const state = await this.loadAclState(params.user);
|
|
163
|
+
if (!state.exists)
|
|
164
|
+
return null;
|
|
165
|
+
const roleByte = ROLE_TO_BYTE[params.role];
|
|
166
|
+
const pageIdx = findPageContaining(state, params.asset, roleByte);
|
|
167
|
+
if (pageIdx === null)
|
|
168
|
+
return null;
|
|
169
|
+
const [aclConfigPda] = await getAclConfigPDA(params.user, this.antProgram);
|
|
170
|
+
const [aclPagePda] = await getAclPagePDA(params.user, pageIdx, this.antProgram);
|
|
171
|
+
return { aclConfigPda, aclPagePda, pageIdx };
|
|
172
|
+
}
|
|
173
|
+
// =========================================
|
|
174
|
+
// Low-level instruction builders (1:1 with contract handlers)
|
|
175
|
+
// =========================================
|
|
176
|
+
//
|
|
177
|
+
// Each helper closes over `this.antProgram` + `this.signer`, so call
|
|
178
|
+
// sites inside `planAclMaintenance` (and any external consumer that
|
|
179
|
+
// wants raw control) don't need to thread program id / payer through
|
|
180
|
+
// every call. Page indices are `bigint` to match the on-chain `u64`
|
|
181
|
+
// schema — see `docs/ACCOUNT_SCALING_PATTERNS.md` for why we
|
|
182
|
+
// standardised on `u64` across paginated shapes.
|
|
183
|
+
/**
|
|
184
|
+
* Build a `register_acl_config` instruction. Permissionless: any wallet
|
|
185
|
+
* can pay to bootstrap an ACL head for any user.
|
|
186
|
+
*/
|
|
187
|
+
async buildRegisterAclConfigIx(params) {
|
|
188
|
+
const [aclConfig] = await getAclConfigPDA(params.user, this.antProgram);
|
|
189
|
+
return getRegisterAclConfigInstruction({ aclConfig, payer: this.signer, user: params.user }, { programAddress: this.antProgram });
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Build an `add_acl_page` instruction that appends the next page (i.e.
|
|
193
|
+
* page `page_count`) to a user's ACL. Caller must derive `pageIdx` from
|
|
194
|
+
* a fresh read of `AclConfig.page_count` to avoid colliding with an
|
|
195
|
+
* existing PDA — `planAclMaintenance` does this for you.
|
|
196
|
+
*/
|
|
197
|
+
async buildAddAclPageIx(params) {
|
|
198
|
+
const [aclConfig] = await getAclConfigPDA(params.user, this.antProgram);
|
|
199
|
+
const [aclPage] = await getAclPagePDA(params.user, params.pageIdx, this.antProgram);
|
|
200
|
+
return getAddAclPageInstruction({ aclConfig, aclPage, payer: this.signer }, { programAddress: this.antProgram });
|
|
201
|
+
}
|
|
202
|
+
/** Build a `record_acl_*` instruction for the given role. */
|
|
203
|
+
async buildRecordIx(params) {
|
|
204
|
+
const [aclConfig] = await getAclConfigPDA(params.user, this.antProgram);
|
|
205
|
+
const [aclPage] = await getAclPagePDA(params.user, params.pageIdx, this.antProgram);
|
|
206
|
+
if (params.role === 'owner') {
|
|
207
|
+
return getRecordAclOwnerInstructionAsync({ asset: params.asset, aclConfig, aclPage, payer: this.signer }, { programAddress: this.antProgram });
|
|
208
|
+
}
|
|
209
|
+
return getRecordAclControllerInstructionAsync({ asset: params.asset, aclConfig, aclPage, payer: this.signer }, { programAddress: this.antProgram });
|
|
210
|
+
}
|
|
211
|
+
/** Build a `remove_acl_*` instruction for the given role. */
|
|
212
|
+
async buildRemoveIx(params) {
|
|
213
|
+
const [aclConfig] = await getAclConfigPDA(params.user, this.antProgram);
|
|
214
|
+
const [aclPage] = await getAclPagePDA(params.user, params.pageIdx, this.antProgram);
|
|
215
|
+
if (params.role === 'owner') {
|
|
216
|
+
return getRemoveAclOwnerInstructionAsync({ asset: params.asset, aclConfig, aclPage, payer: this.signer }, { programAddress: this.antProgram });
|
|
217
|
+
}
|
|
218
|
+
return getRemoveAclControllerInstructionAsync({ asset: params.asset, aclConfig, aclPage, payer: this.signer }, { programAddress: this.antProgram });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Close the trailing `AclPage` (must be the last page and empty).
|
|
222
|
+
* Returns rent to `beneficiary`, which the on-chain handler enforces
|
|
223
|
+
* equals `acl_config.user`.
|
|
224
|
+
*/
|
|
225
|
+
async buildCloseAclPageIx(params) {
|
|
226
|
+
const [aclConfig] = await getAclConfigPDA(params.user, this.antProgram);
|
|
227
|
+
const [aclPage] = await getAclPagePDA(params.user, params.pageIdx, this.antProgram);
|
|
228
|
+
return getCloseAclPageInstruction({ aclConfig, aclPage, beneficiary: params.beneficiary }, { programAddress: this.antProgram });
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Close `AclConfig` once `page_count == 0` and `total_entries == 0`.
|
|
232
|
+
* Returns rent to `beneficiary`, which must equal `acl_config.user`
|
|
233
|
+
* on-chain.
|
|
234
|
+
*/
|
|
235
|
+
async buildCloseAclConfigIx(params) {
|
|
236
|
+
const [aclConfig] = await getAclConfigPDA(params.user, this.antProgram);
|
|
237
|
+
return getCloseAclConfigInstruction({ aclConfig, beneficiary: params.beneficiary }, { programAddress: this.antProgram });
|
|
238
|
+
}
|
|
239
|
+
// =========================================
|
|
240
|
+
// High-level workflow helpers
|
|
241
|
+
// =========================================
|
|
242
|
+
//
|
|
243
|
+
// These wrap `planAclMaintenance` with domain language so call sites
|
|
244
|
+
// never need to assemble raw `AclMaintenanceOp[]` arrays. The planner
|
|
245
|
+
// is kept as a `protected` building block — useful for future bulk
|
|
246
|
+
// flows but not part of the public surface.
|
|
247
|
+
/**
|
|
248
|
+
* Build the ACL ixs needed to bootstrap a freshly-spawned ANT's
|
|
249
|
+
* paginated owner ACL. The on-chain `initialize` handler seeds
|
|
250
|
+
* `ant_controllers = vec![owner]` (matches the Lua source), so the
|
|
251
|
+
* owner is recorded under **both** roles: `Owner` (for "ANTs I own"
|
|
252
|
+
* lookups) and `Controller` (for "ANTs I can manage" lookups).
|
|
253
|
+
*
|
|
254
|
+
* Returns the instructions in dependency order — `register_acl_config`
|
|
255
|
+
* → `add_acl_page` → `record_acl_owner` → `record_acl_controller`.
|
|
256
|
+
* Caller bundles them into the same tx as the MPL Core create + ANT
|
|
257
|
+
* `initialize` ixs so the ACL is atomic with the spawn.
|
|
258
|
+
*/
|
|
259
|
+
async bootstrapOwnerOnSpawn(params) {
|
|
260
|
+
return this.planAclMaintenance([
|
|
261
|
+
{
|
|
262
|
+
action: 'record',
|
|
263
|
+
role: 'owner',
|
|
264
|
+
user: params.owner,
|
|
265
|
+
asset: params.asset,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
action: 'record',
|
|
269
|
+
role: 'controller',
|
|
270
|
+
user: params.owner,
|
|
271
|
+
asset: params.asset,
|
|
272
|
+
},
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Build the `remove_acl_controller` ixs needed to clean up an ANT's
|
|
277
|
+
* ex-controller ACL entries after a transfer. The contract handler
|
|
278
|
+
* for `transfer` cannot atomically `swap_remove` each ex-controller
|
|
279
|
+
* (variable-length, no clean Codama representation), so the SDK
|
|
280
|
+
* bundles them via this helper instead.
|
|
281
|
+
*
|
|
282
|
+
* Idempotent (skips controllers whose ACL entry is already absent),
|
|
283
|
+
* so it's safe to call against a stale snapshot of `AntControllers`.
|
|
284
|
+
*/
|
|
285
|
+
async bulkRemoveControllerEntries(params) {
|
|
286
|
+
if (params.controllers.length === 0)
|
|
287
|
+
return [];
|
|
288
|
+
return this.planAclMaintenance(params.controllers.map((user) => ({
|
|
289
|
+
action: 'remove',
|
|
290
|
+
role: 'controller',
|
|
291
|
+
user,
|
|
292
|
+
asset: params.asset,
|
|
293
|
+
})));
|
|
294
|
+
}
|
|
295
|
+
// =========================================
|
|
296
|
+
// Preflight planner (internal)
|
|
297
|
+
// =========================================
|
|
298
|
+
/**
|
|
299
|
+
* Resolve an array of desired ACL mutations into the minimum
|
|
300
|
+
* instruction set needed to make them happen. For each unique user:
|
|
301
|
+
* - reads the `AclConfig` head once (and all `AclPage`s in one
|
|
302
|
+
* `getMultipleAccountsInfo` round trip)
|
|
303
|
+
* - prepends `register_acl_config` if the head is missing AND any
|
|
304
|
+
* `action: 'record'` op targets that user
|
|
305
|
+
* - emits `add_acl_page` whenever the existing pages have no room
|
|
306
|
+
* - drops `action: 'record'` ops where the entry already exists (no-op)
|
|
307
|
+
* - drops `action: 'remove'` ops where the entry is already absent (no-op)
|
|
308
|
+
*
|
|
309
|
+
* Returned instructions preserve the dependency order:
|
|
310
|
+
* `register_acl_config` → `add_acl_page` → record/remove ixs that
|
|
311
|
+
* target the new page.
|
|
312
|
+
*
|
|
313
|
+
* Page selection on append: we fill the **first non-full page** so
|
|
314
|
+
* density recovers naturally after a `swap_remove` made a mid-life
|
|
315
|
+
* page sparse (see `docs/ACCOUNT_SCALING_PATTERNS.md` § Pattern C).
|
|
316
|
+
*
|
|
317
|
+
* `protected` so subclasses (and the workflow helpers above) can call
|
|
318
|
+
* it, but external callers go through `bootstrapOwnerOnSpawn` /
|
|
319
|
+
* `bulkRemoveControllerEntries` instead.
|
|
320
|
+
*/
|
|
321
|
+
async planAclMaintenance(ops) {
|
|
322
|
+
if (ops.length === 0)
|
|
323
|
+
return [];
|
|
324
|
+
// Dedupe users to load each AclConfig at most once.
|
|
325
|
+
const userKeys = new Map();
|
|
326
|
+
for (const op of ops) {
|
|
327
|
+
userKeys.set(op.user, address(op.user));
|
|
328
|
+
}
|
|
329
|
+
const states = new Map();
|
|
330
|
+
await Promise.all(Array.from(userKeys.entries()).map(async ([key, addr]) => {
|
|
331
|
+
states.set(key, await this.loadAclState(addr));
|
|
332
|
+
}));
|
|
333
|
+
const instructions = [];
|
|
334
|
+
const registered = new Set();
|
|
335
|
+
// First pass: prepend register_acl_config for any user with a
|
|
336
|
+
// `record` op whose head does not yet exist. Mutate the cached state
|
|
337
|
+
// so subsequent ops in this batch see the soon-to-exist config as
|
|
338
|
+
// empty (not missing).
|
|
339
|
+
for (const op of ops) {
|
|
340
|
+
if (op.action !== 'record')
|
|
341
|
+
continue;
|
|
342
|
+
const state = states.get(op.user);
|
|
343
|
+
if (!state.exists && !registered.has(op.user)) {
|
|
344
|
+
instructions.push(await this.buildRegisterAclConfigIx({ user: address(op.user) }));
|
|
345
|
+
registered.add(op.user);
|
|
346
|
+
state.exists = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Second pass: emit each mutation iff it would change observable
|
|
350
|
+
// state. Pick a destination page per record op (filling the first
|
|
351
|
+
// non-full page, emitting `add_acl_page` when needed) and target the
|
|
352
|
+
// holding page for each remove op.
|
|
353
|
+
for (const op of ops) {
|
|
354
|
+
const state = states.get(op.user);
|
|
355
|
+
const userAddr = address(op.user);
|
|
356
|
+
const assetAddr = address(op.asset);
|
|
357
|
+
const roleByte = ROLE_TO_BYTE[op.role];
|
|
358
|
+
if (op.action === 'record') {
|
|
359
|
+
// Idempotent: skip if the entry is already present somewhere.
|
|
360
|
+
if (findPageContaining(state, op.asset, roleByte) !== null)
|
|
361
|
+
continue;
|
|
362
|
+
let pageIdx = findPageWithRoom(state);
|
|
363
|
+
if (pageIdx === null) {
|
|
364
|
+
pageIdx = state.pageCount;
|
|
365
|
+
if (!state.pendingNewPages.has(pageIdx)) {
|
|
366
|
+
instructions.push(await this.buildAddAclPageIx({ user: userAddr, pageIdx }));
|
|
367
|
+
state.pendingNewPages.add(pageIdx);
|
|
368
|
+
state.pages.set(pageIdx, { entries: [] });
|
|
369
|
+
state.pageCount += 1n;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
instructions.push(await this.buildRecordIx({
|
|
373
|
+
user: userAddr,
|
|
374
|
+
asset: assetAddr,
|
|
375
|
+
role: op.role,
|
|
376
|
+
pageIdx,
|
|
377
|
+
}));
|
|
378
|
+
state.pages
|
|
379
|
+
.get(pageIdx)
|
|
380
|
+
.entries.push({ asset: op.asset, role: roleByte });
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
// action === 'remove'
|
|
384
|
+
if (!state.exists)
|
|
385
|
+
continue;
|
|
386
|
+
const pageIdx = findPageContaining(state, op.asset, roleByte);
|
|
387
|
+
if (pageIdx === null)
|
|
388
|
+
continue;
|
|
389
|
+
instructions.push(await this.buildRemoveIx({
|
|
390
|
+
user: userAddr,
|
|
391
|
+
asset: assetAddr,
|
|
392
|
+
role: op.role,
|
|
393
|
+
pageIdx,
|
|
394
|
+
}));
|
|
395
|
+
// Mirror the on-chain `swap_remove`: drop the matching entry and
|
|
396
|
+
// let the last entry in the page take its slot, so subsequent
|
|
397
|
+
// `findPageContaining` lookups for that page remain correct.
|
|
398
|
+
const page = state.pages.get(pageIdx);
|
|
399
|
+
const i = page.entries.findIndex((e) => e.asset === op.asset && e.role === roleByte);
|
|
400
|
+
if (i >= 0) {
|
|
401
|
+
const last = page.entries.pop();
|
|
402
|
+
if (last && i < page.entries.length) {
|
|
403
|
+
page.entries[i] = last;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return instructions;
|
|
408
|
+
}
|
|
409
|
+
/** Load the head + every page for `user` in two RPC calls. */
|
|
410
|
+
async loadAclState(user) {
|
|
411
|
+
const [configPda] = await getAclConfigPDA(user, this.antProgram);
|
|
412
|
+
const configAccount = await getAccountInfoLegacy(this.rpc, configPda, this.commitment);
|
|
413
|
+
if (!configAccount) {
|
|
414
|
+
return {
|
|
415
|
+
exists: false,
|
|
416
|
+
pages: new Map(),
|
|
417
|
+
pageCount: 0n,
|
|
418
|
+
pendingNewPages: new Set(),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const config = deserializeAclConfig(configAccount.data);
|
|
422
|
+
const pageAddresses = [];
|
|
423
|
+
for (let i = 0n; i < config.pageCount; i++) {
|
|
424
|
+
const [pagePda] = await getAclPagePDA(user, i, this.antProgram);
|
|
425
|
+
pageAddresses.push(pagePda);
|
|
426
|
+
}
|
|
427
|
+
const pageAccounts = await getMultipleAccountsInfoLegacy(this.rpc, pageAddresses, this.commitment);
|
|
428
|
+
const pages = new Map();
|
|
429
|
+
for (let i = 0; i < pageAccounts.length; i++) {
|
|
430
|
+
const acct = pageAccounts[i];
|
|
431
|
+
if (!acct)
|
|
432
|
+
continue;
|
|
433
|
+
const decoded = deserializeAclPage(acct.data);
|
|
434
|
+
pages.set(decoded.pageIdx, {
|
|
435
|
+
entries: decoded.entries.map((e) => ({ asset: e.asset, role: e.role })),
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
exists: true,
|
|
440
|
+
pages,
|
|
441
|
+
pageCount: config.pageCount,
|
|
442
|
+
pendingNewPages: new Set(),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Find the first page that has room for one more entry. Returns `null`
|
|
448
|
+
* if every existing page is at `MAX_ACL_PAGE_ENTRIES` — caller emits an
|
|
449
|
+
* `add_acl_page` and uses the new page idx instead.
|
|
450
|
+
*/
|
|
451
|
+
function findPageWithRoom(state) {
|
|
452
|
+
for (let i = 0n; i < state.pageCount; i++) {
|
|
453
|
+
const page = state.pages.get(i);
|
|
454
|
+
// Missing pages are treated as full so we don't accidentally "fill" a
|
|
455
|
+
// page the program will reject. In practice this only happens if the
|
|
456
|
+
// RPC returned partial results; the worst case is one extra
|
|
457
|
+
// `add_acl_page` ix.
|
|
458
|
+
if (!page)
|
|
459
|
+
continue;
|
|
460
|
+
if (page.entries.length < MAX_ACL_PAGE_ENTRIES)
|
|
461
|
+
return i;
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
/** Find which page currently holds `(asset, role)`. */
|
|
466
|
+
function findPageContaining(state, asset, role) {
|
|
467
|
+
for (const [idx, page] of state.pages) {
|
|
468
|
+
if (page.entries.some((e) => e.asset === asset && e.role === role))
|
|
469
|
+
return idx;
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|