@sorandomains/owner 0.6.0 → 0.8.0-migration.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/dist/index.d.ts +62 -37
- package/dist/index.js +209 -11
- package/dist/native-allowlist.d.ts +21 -0
- package/dist/native-allowlist.js +34 -0
- package/dist/native-auth.d.ts +31 -0
- package/dist/native-auth.js +85 -0
- package/dist/native-code-policy.d.ts +14 -0
- package/dist/native-code-policy.js +53 -0
- package/dist/native-codec.d.ts +38 -0
- package/dist/native-codec.js +95 -0
- package/dist/native-history.d.ts +26 -0
- package/dist/native-history.js +162 -0
- package/dist/native-transport.d.ts +66 -0
- package/dist/native-transport.js +137 -0
- package/dist/native-types.d.ts +166 -0
- package/dist/native-types.js +98 -0
- package/dist/payment.d.ts +37 -0
- package/dist/payment.js +148 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
# @sorandomains/owner
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> ledger
|
|
3
|
+
|
|
4
|
+
> Native claim testnet deployment verified at ledger 4534629 on 6 September 2026 (12:12 UTC). See the
|
|
5
5
|
> [release status](https://docs.soran.domains/reference/release-status) for package and service availability.
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Preview 0.8.0-migration.0 targets Stellar SDK17 (`>=17 <18`). ASCII names
|
|
9
9
|
and labels are validated before lowercase normalization; Unicode lookalikes are
|
|
10
10
|
rejected. Writes continue to target the owning Registry/Registrar/Resolver. Universal
|
|
11
|
-
Lookup is the read entry point in `@sorandomains/lookup` 0.
|
|
11
|
+
Lookup is the read entry point in `@sorandomains/lookup` 0.8.0.
|
|
12
12
|
|
|
13
13
|
Ownership, issuer, holder and treasury inputs remain G/C account or contract
|
|
14
14
|
addresses. Muxed M addresses are payment destinations only: issue the name to its
|
|
15
15
|
actual G/C holder, then that holder can call Holder `setPayment` to publish M.
|
|
16
16
|
|
|
17
17
|
Run your Soran namespace on Stellar, programmatically. Issue names to your
|
|
18
|
-
users, manage their lifecycle
|
|
19
|
-
ready — walk the one-way permanence door. Every operation is a transaction
|
|
18
|
+
users, manage their lifecycle and transfer the namespace. Every operation is a transaction
|
|
20
19
|
your own key signs and any Soroban RPC node submits: no Soran account, no
|
|
21
20
|
hosted API in the path.
|
|
22
21
|
|
|
@@ -45,7 +44,7 @@ for (const o of batch.outcomes) if (!o.issued) console.warn(o.label, o.reason);
|
|
|
45
44
|
| `reclaim` | Take a name back — only where the namespace policy allows it |
|
|
46
45
|
| `renew` | Extend a finite-term name; returns the new expiry |
|
|
47
46
|
| `setTreasury` | Route reclaim custody to a treasury address |
|
|
48
|
-
| `makePermanent` |
|
|
47
|
+
| `makePermanent` | Legacy deployment operation; unavailable on governed testnet contracts |
|
|
49
48
|
| `proposeNamespaceTransfer` / `acceptNamespaceTransfer` / `cancelNamespaceTransfer` | Two-step, accept-to-move namespace transfer |
|
|
50
49
|
| `setResolver` | Point the namespace at a resolver (frozen once permanent) |
|
|
51
50
|
| `policy` / `isPermanent` / `nameState` / `pendingNamespaceTransfer` / `namespaceOwner` / `registrarOf` / `assertOwner` | Reads that support the write flows |
|
|
@@ -83,14 +82,42 @@ code at all.
|
|
|
83
82
|
|
|
84
83
|
Docs: <https://github.com/SoranDomains/docs> · License: MIT
|
|
85
84
|
|
|
85
|
+
|
|
86
|
+
## Native username claiming
|
|
87
|
+
|
|
88
|
+
`claimPolicy` and `nativeClaimCapability` read the current on-chain setup. Configure it with `configureClaims`, `setClaimsEnabled`/`pauseClaims`, `reserveNames`, `releaseReservations` and `assignReserved`. Manual mode retains ordinary owner issuance; Public mode routes ordinary issuance through the public claim rules, while explicit reserved assignment remains an owner power. The default disabled policy is not an open registration service.
|
|
89
|
+
|
|
90
|
+
Read the [native claim APIs, security boundaries and complete signup flow](https://github.com/SoranDomains/sdk/blob/main/NATIVE-CLAIMS.md). G/no memo, G with ID/Text/Hash, full M/no separate memo and C/no memo remain supported payment destinations. Current transaction-signing adapters use classic G accounts.
|
|
91
|
+
|
|
86
92
|
## Verified testnet deployment
|
|
87
93
|
|
|
88
|
-
Verified on
|
|
94
|
+
Verified on 6 September 2026 at ledger **4534629** (12:12 UTC). Network passphrase: `Test SDF Network ; September 2015`.
|
|
89
95
|
|
|
90
96
|
| Contract | Address |
|
|
91
97
|
|---|---|
|
|
92
|
-
| Registry | `
|
|
98
|
+
| Registry | `CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK` |
|
|
93
99
|
|
|
94
100
|
Mainnet has no deployment preset. Custom networks must supply their own verified
|
|
95
101
|
addresses. Universal Lookup upgrades remain immediately executable; an address
|
|
96
102
|
and ABI version do not pin the code that will execute after a governance upgrade.
|
|
103
|
+
|
|
104
|
+
## Network fee limits
|
|
105
|
+
|
|
106
|
+
Available in 0.8.0.
|
|
107
|
+
|
|
108
|
+
Every write and automatic restoration has a total network-fee limit, including the base and resource fees. The default is **5 XLM** (`50_000_000n` stroops). Set `maxNetworkFeeStroops` on the client only after reviewing a higher storage/rent estimate. This limits each transaction separately; it is not a daily or batch spending budget, and namespace/username prices are separate.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const client = new SoranOwner({
|
|
112
|
+
signer,
|
|
113
|
+
maxNetworkFeeStroops: 50_000_000n,
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The older `maxNativeFeeStroops` option remains supported: when the new option is omitted, it also supplies the total ceiling. If both are set, native operations use the lower value and other writes use `maxNetworkFeeStroops`. An excessive estimate is rejected before wallet approval. After a possible submission, errors retain the original transaction hash; reconcile it before creating a replacement transaction.
|
|
118
|
+
|
|
119
|
+
## Governed testnet code and migration recovery
|
|
120
|
+
|
|
121
|
+
On a governed Registry, native verification reads the exact per-namespace Registrar code pin and its upgrade history. Approved upgrades do not have to match the current factory default. Missing or malformed provenance still prevents signing and receipt confirmation; RPC failure never downgrades verification to a legacy rule.
|
|
122
|
+
|
|
123
|
+
Historical claim recovery is read-only and binds the original intent to the frozen source Registrar and sealed migration commitments. It never rewrites the intent to a successor Registry or treats an unavailable receipt as permission to submit again. Deployment migration and package publication are separate; check the release status for the active addresses.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,43 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
* TRUST MODEL. This SDK talks to the contracts directly over any Soroban RPC
|
|
13
|
-
* node — no Soran account, no hosted API, no third party in the path. Every
|
|
14
|
-
* operation here is authorized on chain: the Registrar and Registry check
|
|
15
|
-
* `require_auth` against the namespace owner's address, so the only credential
|
|
16
|
-
* that matters is the signer you supply. Soran's servers cannot perform any of
|
|
17
|
-
* these operations for you or against you.
|
|
18
|
-
*
|
|
19
|
-
* WHO CAN CALL WHAT. All operations below are namespace-OWNER powers except
|
|
20
|
-
* `acceptNamespaceTransfer`, which the proposed NEW owner signs. Name-holder
|
|
21
|
-
* powers (transferring an individual name, pointing it at a new address,
|
|
22
|
-
* electing a primary name) are the holder's alone — they live in the contracts'
|
|
23
|
-
* holder-authorized entry points and live in `@sorandomains/holder`; this
|
|
24
|
-
* package deliberately cannot exercise them.
|
|
25
|
-
*
|
|
26
|
-
* SIGNING. Supply a `TxSigner`: `keypairSigner(secret)` for backends, or wrap a
|
|
27
|
-
* browser wallet (Freighter / Stellar Wallets Kit expose exactly this
|
|
28
|
-
* `signTransaction(xdr, { networkPassphrase })` shape). The SDK builds and
|
|
29
|
-
* simulates each transaction, hands you the envelope to sign, and submits it.
|
|
30
|
-
* Operations on one SoranOwner instance are serialized so concurrent calls
|
|
31
|
-
* cannot race the account sequence number.
|
|
32
|
-
*
|
|
33
|
-
* READS. For resolution, reverse lookup, and assurance checks use the read-only
|
|
34
|
-
* companion package `@sorandomains/lookup` — wallets and apps should depend on
|
|
35
|
-
* that one only. The few reads here (policy, pending transfers, records) exist
|
|
36
|
-
* to support write flows.
|
|
37
|
-
*/
|
|
1
|
+
import { type HistoricalRecoveryOptions } from "./native-history.js";
|
|
2
|
+
import type { ClaimIntent } from "./native-types.js";
|
|
3
|
+
export type { HistoricalRecoveryOptions } from "./native-history.js";
|
|
4
|
+
import { type NativeWriteOptions, type NativeCapability } from "./native-transport.js";
|
|
5
|
+
import { type ClaimSettings, type ClaimConfig, type ClaimUsage, type ApprovalUsage } from "./native-types.js";
|
|
6
|
+
export * from "./native-types.js";
|
|
7
|
+
export * from "./native-allowlist.js";
|
|
8
|
+
export { NativeClaimError, namespaceNode as nativeNamespaceNode, paymentDestinationToScVal } from "./native-codec.js";
|
|
9
|
+
export { validateNativeTransaction, authorizedInvocation, type NativeAuthorizationPlan } from "./native-auth.js";
|
|
10
|
+
export type { NativeCapability, NativePrepared, NativeWriteOptions } from "./native-transport.js";
|
|
38
11
|
/** Known public deployments. Pass explicit options for anything else. */
|
|
39
12
|
export declare const DEPLOYMENTS: {
|
|
40
13
|
readonly testnet: {
|
|
14
|
+
readonly rpcUrl: "https://soroban-testnet.stellar.org";
|
|
15
|
+
readonly passphrase: string;
|
|
16
|
+
readonly registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK";
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
20
|
+
export declare const LEGACY_DEPLOYMENTS: {
|
|
21
|
+
readonly testnet20260905: {
|
|
41
22
|
readonly rpcUrl: "https://soroban-testnet.stellar.org";
|
|
42
23
|
readonly passphrase: string;
|
|
43
24
|
readonly registryId: "CASORANI5CN2NJFEO2MGTRDA35AOEF3D3OCVBWN3FS6B6FXNQ74RTJ7H";
|
|
@@ -130,6 +111,11 @@ export type NameState = {
|
|
|
130
111
|
generation: bigint;
|
|
131
112
|
};
|
|
132
113
|
export type OwnerOptions = {
|
|
114
|
+
/** Maximum total network fee per transaction, including restoration. Default 5 XLM.
|
|
115
|
+
* If omitted, an explicit maxNativeFeeStroops also supplies this ceiling. */
|
|
116
|
+
maxNetworkFeeStroops?: bigint;
|
|
117
|
+
/** Optional additional native-operation ceiling; the lower configured limit wins. */
|
|
118
|
+
maxNativeFeeStroops?: bigint;
|
|
133
119
|
/** Signs every transaction. The namespace owner's account — or, for
|
|
134
120
|
* `acceptNamespaceTransfer`, the proposed new owner's. */
|
|
135
121
|
signer: TxSigner;
|
|
@@ -150,6 +136,8 @@ export type OwnerOptions = {
|
|
|
150
136
|
fee?: string;
|
|
151
137
|
};
|
|
152
138
|
export declare class SoranOwner {
|
|
139
|
+
private maxNativeFeeStroops;
|
|
140
|
+
private maxNetworkFeeStroops;
|
|
153
141
|
private server;
|
|
154
142
|
private passphrase;
|
|
155
143
|
private registryId;
|
|
@@ -161,6 +149,40 @@ export declare class SoranOwner {
|
|
|
161
149
|
private static REGISTRAR_TTL_MS;
|
|
162
150
|
constructor(opts: OwnerOptions);
|
|
163
151
|
/** The namespace's on-chain attested Registrar id. Cached per instance. */
|
|
152
|
+
private nativeConfirmed;
|
|
153
|
+
private nativeContext;
|
|
154
|
+
/** Read the original frozen-source receipt after a fully sealed migration. Never submits. */
|
|
155
|
+
recoverHistoricalClaim(intent: ClaimIntent, options: HistoricalRecoveryOptions): Promise<import("./native-types.js").ClaimReceipt | null>;
|
|
156
|
+
/** Native successor capability. A failed RPC read never means open or legacy. */
|
|
157
|
+
nativeClaimCapability(namespace: string): Promise<NativeCapability>;
|
|
158
|
+
claimPolicy(namespace: string): Promise<ClaimConfig | null>;
|
|
159
|
+
nativeClaimFeeToken(namespace: string): Promise<string>;
|
|
160
|
+
private nativeOwnerWrite;
|
|
161
|
+
configureClaims(namespace: string, settings: ClaimSettings, options?: NativeWriteOptions): Promise<Submitted & {
|
|
162
|
+
config: ClaimConfig;
|
|
163
|
+
}>;
|
|
164
|
+
setClaimsEnabled(namespace: string, enabled: boolean, options?: NativeWriteOptions): Promise<Submitted & {
|
|
165
|
+
config: ClaimConfig;
|
|
166
|
+
}>;
|
|
167
|
+
pauseClaims(namespace: string, options?: NativeWriteOptions): Promise<Submitted & {
|
|
168
|
+
config: ClaimConfig;
|
|
169
|
+
}>;
|
|
170
|
+
private updateReservations;
|
|
171
|
+
reserveNames(namespace: string, labels: readonly string[], options?: NativeWriteOptions): Promise<{
|
|
172
|
+
hash: string;
|
|
173
|
+
ledger: number;
|
|
174
|
+
updated: number;
|
|
175
|
+
}>;
|
|
176
|
+
releaseReservations(namespace: string, labels: readonly string[], options?: NativeWriteOptions): Promise<{
|
|
177
|
+
hash: string;
|
|
178
|
+
ledger: number;
|
|
179
|
+
updated: number;
|
|
180
|
+
}>;
|
|
181
|
+
isReserved(namespace: string, label: string): Promise<boolean>;
|
|
182
|
+
/** Privileged reserved assignment publishes the holder's default route; no custom destination is implied. */
|
|
183
|
+
assignReserved(namespace: string, label: string, holder: string, options?: NativeWriteOptions): Promise<IssueResult>;
|
|
184
|
+
nativeClaimUsage(namespace: string, holder: string): Promise<ClaimUsage>;
|
|
185
|
+
nativeApprovalUsage(namespace: string): Promise<ApprovalUsage>;
|
|
164
186
|
registrarOf(namespace: string): Promise<string>;
|
|
165
187
|
/** The namespace's current owner on the Registry, or null if unallocated. */
|
|
166
188
|
namespaceOwner(namespace: string): Promise<string | null>;
|
|
@@ -232,6 +254,9 @@ export declare class SoranOwner {
|
|
|
232
254
|
private readRecords;
|
|
233
255
|
/** Simulation-only contract read — no signature, no fee, no state change. */
|
|
234
256
|
private read;
|
|
257
|
+
private readWithLedger;
|
|
258
|
+
private simulateRead;
|
|
259
|
+
private assertNetworkFee;
|
|
235
260
|
private signEnvelope;
|
|
236
261
|
private serialize;
|
|
237
262
|
/** The signer's live account (sequence source) — with an actionable error
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recoverFrozenClaim, historicalReadLedger } from "./native-history.js";
|
|
1
2
|
/**
|
|
2
3
|
* @sorandomains/owner — the write-side SDK for Soran namespace owners.
|
|
3
4
|
*
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
* that one only. The few reads here (policy, pending transfers, records) exist
|
|
36
37
|
* to support write flows.
|
|
37
38
|
*/
|
|
38
|
-
import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, xdr, } from "@stellar/stellar-sdk";
|
|
39
|
+
import { Account, Address, BASE_FEE, Contract, Keypair, Networks, Operation, StrKey, SorobanDataBuilder, TransactionBuilder, hash, nativeToScVal, rpc, scValToNative, xdr, } from "@stellar/stellar-sdk";
|
|
39
40
|
/** Contract events from a transaction's meta, across meta versions (v3 puts
|
|
40
41
|
* them on sorobanMeta; v4 nests them per-operation and in tx events). */
|
|
41
42
|
function extractContractEvents(meta) {
|
|
@@ -84,12 +85,29 @@ function decodeIssuedEvents(events, registrarId) {
|
|
|
84
85
|
}
|
|
85
86
|
return out;
|
|
86
87
|
}
|
|
88
|
+
import { NativeClaimError, address as nativeAddress, bool as nativeBool, bytes32 as nativeBytes32, hex as nativeHex, sc as nativeSc } from "./native-codec.js";
|
|
89
|
+
import { requireNative, nativeCapability, sendNative } from "./native-transport.js";
|
|
90
|
+
import { claimSettingsToScVal, claimConfigFromNative, claimLabelToScVal, claimUsageFromNative, approvalUsageFromNative } from "./native-types.js";
|
|
91
|
+
export * from "./native-types.js";
|
|
92
|
+
export * from "./native-allowlist.js";
|
|
93
|
+
export { NativeClaimError, namespaceNode as nativeNamespaceNode, paymentDestinationToScVal } from "./native-codec.js";
|
|
94
|
+
export { validateNativeTransaction, authorizedInvocation } from "./native-auth.js";
|
|
87
95
|
// ---------------------------------------------------------------------------
|
|
88
96
|
// Deployments
|
|
89
97
|
// ---------------------------------------------------------------------------
|
|
90
98
|
/** Known public deployments. Pass explicit options for anything else. */
|
|
91
99
|
export const DEPLOYMENTS = {
|
|
92
100
|
testnet: {
|
|
101
|
+
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
102
|
+
passphrase: Networks.TESTNET,
|
|
103
|
+
// The immutable Registry. The Registrar for a namespace is discovered on
|
|
104
|
+
// chain via `registrar_of(node)` — never configured by hand.
|
|
105
|
+
registryId: "CBSORANPM664QXYMYRZKLQDQE2TXFSK4GBMC6EIRSRTAUZRZCRZRFNMK",
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
/** Explicit historical deployment access. Names in different Registries are separate identities; never a fallback. */
|
|
109
|
+
export const LEGACY_DEPLOYMENTS = {
|
|
110
|
+
testnet20260905: {
|
|
93
111
|
rpcUrl: "https://soroban-testnet.stellar.org",
|
|
94
112
|
passphrase: Networks.TESTNET,
|
|
95
113
|
// The immutable Registry. The Registrar for a namespace is discovered on
|
|
@@ -134,6 +152,37 @@ const REGISTRAR_ERRORS = {
|
|
|
134
152
|
18: "InvalidPolicy",
|
|
135
153
|
19: "ExpiryOverflow",
|
|
136
154
|
20: "InvalidRegistry",
|
|
155
|
+
21: "InvalidClaimConfig",
|
|
156
|
+
22: "ClaimsNotConfigured",
|
|
157
|
+
23: "ClaimsPaused",
|
|
158
|
+
24: "StaleClaimPolicy",
|
|
159
|
+
25: "ClaimIntentMismatch",
|
|
160
|
+
26: "ClaimIntentExpired",
|
|
161
|
+
27: "ReservedName",
|
|
162
|
+
28: "NameNotReserved",
|
|
163
|
+
29: "PublicIssuanceRequired",
|
|
164
|
+
30: "WalletClaimLimit",
|
|
165
|
+
31: "InvalidEligibility",
|
|
166
|
+
32: "ApprovalAllowanceReached",
|
|
167
|
+
33: "ApprovalRateReached",
|
|
168
|
+
34: "RequestIdConflict",
|
|
169
|
+
35: "CounterOverflow",
|
|
170
|
+
36: "InvalidNativeBinding",
|
|
171
|
+
37: "UnsupportedClaimant",
|
|
172
|
+
38: "DuplicateLabel",
|
|
173
|
+
39: "RenewalTooEarly",
|
|
174
|
+
40: "RenewalLeaseLimit",
|
|
175
|
+
41: "DestinationInitializationFailed",
|
|
176
|
+
42: "FeeSettlementFailed",
|
|
177
|
+
43: "MigrationInProgress",
|
|
178
|
+
44: "MigrationAlreadyStarted",
|
|
179
|
+
45: "MigrationNotActive",
|
|
180
|
+
46: "MigrationMismatch",
|
|
181
|
+
47: "MigrationDuplicate",
|
|
182
|
+
48: "MigrationIncomplete",
|
|
183
|
+
49: "MigrationUnavailable",
|
|
184
|
+
50: "MigrationUnsupported",
|
|
185
|
+
51: "PermanentLockDisabled",
|
|
137
186
|
};
|
|
138
187
|
/** Registry contract error codes, by number. */
|
|
139
188
|
const REGISTRY_ERRORS = {
|
|
@@ -165,6 +214,19 @@ const REGISTRY_ERRORS = {
|
|
|
165
214
|
26: "AnchorMismatch",
|
|
166
215
|
27: "InvalidAllocator",
|
|
167
216
|
28: "StaleEra",
|
|
217
|
+
29: "OwnershipEpochUnavailable",
|
|
218
|
+
30: "OwnershipEpochOverflow",
|
|
219
|
+
31: "MigrationInProgress",
|
|
220
|
+
32: "MigrationAlreadyStarted",
|
|
221
|
+
33: "MigrationNotActive",
|
|
222
|
+
34: "MigrationMismatch",
|
|
223
|
+
35: "MigrationIncomplete",
|
|
224
|
+
36: "MigrationDuplicate",
|
|
225
|
+
37: "MigrationUnsupported",
|
|
226
|
+
38: "MigrationUnavailable",
|
|
227
|
+
39: "UpgradePolicyMismatch",
|
|
228
|
+
40: "UpgradeNotApproved",
|
|
229
|
+
41: "PermanentLockDisabled",
|
|
168
230
|
};
|
|
169
231
|
/**
|
|
170
232
|
* A failed owner operation. When the contract itself rejected the call,
|
|
@@ -256,6 +318,8 @@ export const MAX_BATCH = 23;
|
|
|
256
318
|
// The client
|
|
257
319
|
// ---------------------------------------------------------------------------
|
|
258
320
|
export class SoranOwner {
|
|
321
|
+
maxNativeFeeStroops;
|
|
322
|
+
maxNetworkFeeStroops;
|
|
259
323
|
server;
|
|
260
324
|
passphrase;
|
|
261
325
|
registryId;
|
|
@@ -271,6 +335,13 @@ export class SoranOwner {
|
|
|
271
335
|
registrars = new Map();
|
|
272
336
|
static REGISTRAR_TTL_MS = 30_000;
|
|
273
337
|
constructor(opts) {
|
|
338
|
+
this.maxNetworkFeeStroops = opts?.maxNetworkFeeStroops ?? opts?.maxNativeFeeStroops ?? 50000000n;
|
|
339
|
+
const nativeLimit = opts?.maxNativeFeeStroops ?? this.maxNetworkFeeStroops;
|
|
340
|
+
for (const limit of [this.maxNetworkFeeStroops, nativeLimit]) {
|
|
341
|
+
if (typeof limit !== "bigint" || limit <= 0n || limit > 4294967295n)
|
|
342
|
+
throw new OwnerError("maximum network fee must be a bigint between 1 and 4294967295 stroops");
|
|
343
|
+
}
|
|
344
|
+
this.maxNativeFeeStroops = nativeLimit < this.maxNetworkFeeStroops ? nativeLimit : this.maxNetworkFeeStroops;
|
|
274
345
|
if (!opts?.signer)
|
|
275
346
|
throw new OwnerError("OwnerOptions.signer is required");
|
|
276
347
|
const d = DEPLOYMENTS[opts.network ?? "testnet"];
|
|
@@ -287,9 +358,95 @@ export class SoranOwner {
|
|
|
287
358
|
}
|
|
288
359
|
this.timeoutSecs = t;
|
|
289
360
|
this.fee = opts.fee ?? BASE_FEE;
|
|
361
|
+
if (!/^[1-9][0-9]*$/.test(this.fee) || BigInt(this.fee) > 4294967295n)
|
|
362
|
+
throw new OwnerError("base network fee must be canonical decimal stroops between 1 and 4294967295");
|
|
290
363
|
}
|
|
291
364
|
// ---- discovery -----------------------------------------------------------
|
|
292
365
|
/** The namespace's on-chain attested Registrar id. Cached per instance. */
|
|
366
|
+
nativeConfirmed(result, decode) {
|
|
367
|
+
try {
|
|
368
|
+
return decode(result.value);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
throw new NativeClaimError(`transaction confirmed but its result could not be verified: ${String(error)}`, "pending", result.hash);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
nativeContext() {
|
|
375
|
+
return { registryId: this.registryId, passphrase: this.passphrase, server: this.server, signer: this.signer, fee: this.fee, timeoutSecs: this.timeoutSecs, maxFeeStroops: this.maxNativeFeeStroops,
|
|
376
|
+
read: (id, method, args) => this.read(id, method, args), serialize: work => this.serialize(work) };
|
|
377
|
+
}
|
|
378
|
+
/** Read the original frozen-source receipt after a fully sealed migration. Never submits. */
|
|
379
|
+
recoverHistoricalClaim(intent, options) {
|
|
380
|
+
return recoverFrozenClaim({ registryId: this.registryId, passphrase: this.passphrase, server: this.server,
|
|
381
|
+
readWithLedger: (id, method, args) => this.readWithLedger(id, method, args) }, intent, options);
|
|
382
|
+
}
|
|
383
|
+
/** Native successor capability. A failed RPC read never means open or legacy. */
|
|
384
|
+
nativeClaimCapability(namespace) { return nativeCapability(this.nativeContext(), normalizeLabel(namespace)); }
|
|
385
|
+
async claimPolicy(namespace) {
|
|
386
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
387
|
+
const raw = await this.read(cap.registrar, "claim_config", []);
|
|
388
|
+
return raw === null ? null : claimConfigFromNative(raw);
|
|
389
|
+
}
|
|
390
|
+
async nativeClaimFeeToken(namespace) {
|
|
391
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
392
|
+
return nativeAddress(await this.read(cap.registrar, "native_fee_token", []), "contract", "native XLM token");
|
|
393
|
+
}
|
|
394
|
+
async nativeOwnerWrite(namespace, method, args, options = {}) {
|
|
395
|
+
return this.serialize(async () => {
|
|
396
|
+
const context = this.nativeContext();
|
|
397
|
+
const cap = await requireNative(context, normalizeLabel(namespace));
|
|
398
|
+
const source = nativeAddress(await this.signer.publicKey(), "account", "owner transaction source");
|
|
399
|
+
if (cap.owner !== source)
|
|
400
|
+
throw new NativeClaimError("the current namespace owner must authorize this change", "authorization");
|
|
401
|
+
return sendNative(context, { source, contract: cap.registrar, method, args, sourceInvocation: { contract: cap.registrar, method, args }, maxFeeStroops: context.maxFeeStroops }, options);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async configureClaims(namespace, settings, options = {}) {
|
|
405
|
+
const encoded = claimSettingsToScVal(settings);
|
|
406
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
407
|
+
const token = nativeAddress(await this.read(cap.registrar, "native_fee_token", []), "contract", "native fee token");
|
|
408
|
+
if (settings.feeToken !== token)
|
|
409
|
+
throw new NativeClaimError("username fee must use this network's canonical XLM token");
|
|
410
|
+
if (settings.admission.type === "approval" && settings.admission.account === cap.owner)
|
|
411
|
+
throw new NativeClaimError("eligibility account must be separate from namespace owner");
|
|
412
|
+
const result = await this.nativeOwnerWrite(namespace, "configure_claims", [encoded], options);
|
|
413
|
+
return { hash: result.hash, ledger: result.ledger, config: this.nativeConfirmed(result, claimConfigFromNative) };
|
|
414
|
+
}
|
|
415
|
+
async setClaimsEnabled(namespace, enabled, options = {}) {
|
|
416
|
+
const result = await this.nativeOwnerWrite(namespace, "set_claim_enabled", [nativeSc.bool(nativeBool(enabled, "claim enabled"))], options);
|
|
417
|
+
return { hash: result.hash, ledger: result.ledger, config: this.nativeConfirmed(result, claimConfigFromNative) };
|
|
418
|
+
}
|
|
419
|
+
pauseClaims(namespace, options = {}) { return this.setClaimsEnabled(namespace, false, options); }
|
|
420
|
+
async updateReservations(namespace, labels, reserved, options) {
|
|
421
|
+
if (!Array.isArray(labels) || labels.length < 1 || labels.length > 23)
|
|
422
|
+
throw new NativeClaimError("reservation batches must contain 1–23 labels");
|
|
423
|
+
const normalized = labels.map(normalizeLabel);
|
|
424
|
+
if (new Set(normalized).size !== normalized.length)
|
|
425
|
+
throw new NativeClaimError("duplicate reservation label");
|
|
426
|
+
const result = await this.nativeOwnerWrite(namespace, "set_reserved", [xdr.ScVal.scvVec(normalized.map(claimLabelToScVal)), nativeSc.bool(reserved)], options);
|
|
427
|
+
if (typeof result.value !== "number" || result.value !== normalized.length)
|
|
428
|
+
throw new NativeClaimError("confirmed reservation result differs from batch; inspect the transaction before retrying", "pending", result.hash);
|
|
429
|
+
return { hash: result.hash, ledger: result.ledger, updated: result.value };
|
|
430
|
+
}
|
|
431
|
+
reserveNames(namespace, labels, options = {}) { return this.updateReservations(namespace, labels, true, options); }
|
|
432
|
+
releaseReservations(namespace, labels, options = {}) { return this.updateReservations(namespace, labels, false, options); }
|
|
433
|
+
async isReserved(namespace, label) {
|
|
434
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
435
|
+
return nativeBool(await this.read(cap.registrar, "is_reserved", [claimLabelToScVal(normalizeLabel(label))]), "reservation");
|
|
436
|
+
}
|
|
437
|
+
/** Privileged reserved assignment publishes the holder's default route; no custom destination is implied. */
|
|
438
|
+
async assignReserved(namespace, label, holder, options = {}) {
|
|
439
|
+
const result = await this.nativeOwnerWrite(namespace, "issue_reserved", [claimLabelToScVal(normalizeLabel(label)), nativeSc.address(nativeAddress(holder, "identity", "reserved holder"))], options);
|
|
440
|
+
return { hash: result.hash, ledger: result.ledger, node: this.nativeConfirmed(result, raw => nativeHex(nativeBytes32(raw, "issued name node"))) };
|
|
441
|
+
}
|
|
442
|
+
async nativeClaimUsage(namespace, holder) {
|
|
443
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
444
|
+
return claimUsageFromNative(await this.read(cap.registrar, "claim_usage", [nativeSc.address(nativeAddress(holder, "identity", "holder"))]));
|
|
445
|
+
}
|
|
446
|
+
async nativeApprovalUsage(namespace) {
|
|
447
|
+
const cap = await requireNative(this.nativeContext(), normalizeLabel(namespace));
|
|
448
|
+
return approvalUsageFromNative(await this.read(cap.registrar, "approval_usage", []));
|
|
449
|
+
}
|
|
293
450
|
async registrarOf(namespace) {
|
|
294
451
|
namespace = normalizeLabel(namespace);
|
|
295
452
|
const hit = this.registrars.get(namespace);
|
|
@@ -544,6 +701,13 @@ export class SoranOwner {
|
|
|
544
701
|
}
|
|
545
702
|
/** Simulation-only contract read — no signature, no fee, no state change. */
|
|
546
703
|
async read(contractId, fn, args) {
|
|
704
|
+
return (await this.simulateRead(contractId, fn, args)).value;
|
|
705
|
+
}
|
|
706
|
+
async readWithLedger(contractId, fn, args) {
|
|
707
|
+
const result = await this.simulateRead(contractId, fn, args);
|
|
708
|
+
return { value: result.value, ledger: historicalReadLedger(result.ledger) };
|
|
709
|
+
}
|
|
710
|
+
async simulateRead(contractId, fn, args) {
|
|
547
711
|
const tx = new TransactionBuilder(new Account(SIM_SOURCE, "0"), {
|
|
548
712
|
fee: BASE_FEE,
|
|
549
713
|
networkPassphrase: this.passphrase,
|
|
@@ -561,9 +725,13 @@ export class SoranOwner {
|
|
|
561
725
|
throw new OwnerError(`${fn}: the on-chain entry is archived (rent lapsed) — any write operation restores it automatically, or restore/touch it first`, contractId, fn);
|
|
562
726
|
}
|
|
563
727
|
if (!rpc.Api.isSimulationSuccess(sim) || !sim.result?.retval)
|
|
564
|
-
return
|
|
728
|
+
throw new OwnerError(`${fn}: missing simulation return value`, contractId, fn);
|
|
565
729
|
const v = scValToNative(sim.result.retval);
|
|
566
|
-
return v === undefined ? null : v;
|
|
730
|
+
return { value: v === undefined ? null : v, ledger: sim.latestLedger };
|
|
731
|
+
}
|
|
732
|
+
assertNetworkFee(fee, fn) {
|
|
733
|
+
if (!/^[1-9][0-9]*$/.test(fee) || BigInt(fee) > this.maxNetworkFeeStroops)
|
|
734
|
+
throw new OwnerError(`${fn}: network fee ${fee} stroops exceeds the configured maximum ${this.maxNetworkFeeStroops} stroops`, null, fn);
|
|
567
735
|
}
|
|
568
736
|
async signEnvelope(xdrBase64) {
|
|
569
737
|
const signed = await this.signer.signTransaction(xdrBase64, {
|
|
@@ -594,7 +762,7 @@ export class SoranOwner {
|
|
|
594
762
|
catch (e) {
|
|
595
763
|
// A stale sequence (another process moved the account) is safe to
|
|
596
764
|
// retry once with a fresh sequence — nothing was included.
|
|
597
|
-
if (/txBadSeq|bad_seq/i.test(String(e))) {
|
|
765
|
+
if (!(e instanceof OwnerError && e.txHash) && /txBadSeq|bad_seq/i.test(String(e))) {
|
|
598
766
|
return await this.attempt(contractId, fn, args, errNames);
|
|
599
767
|
}
|
|
600
768
|
throw e;
|
|
@@ -653,12 +821,15 @@ export class SoranOwner {
|
|
|
653
821
|
throw typedError(contractId, fn, sim.error, errNames);
|
|
654
822
|
}
|
|
655
823
|
const prepared = rpc.assembleTransaction(tx, sim).build();
|
|
824
|
+
this.assertNetworkFee(prepared.fee, fn);
|
|
656
825
|
this.assertSatisfiableAuth(prepared, pub, contractId, fn);
|
|
657
826
|
// The hash is fixed before signatures — compute it now so every failure
|
|
658
827
|
// past this point can carry it (the "re-check before retrying" contract).
|
|
659
828
|
const txHash = toHex(prepared.hash()); // (SDK17) hash() is Uint8Array
|
|
660
829
|
const signed = await this.signEnvelope(prepared.toXDR());
|
|
661
830
|
const envelope = TransactionBuilder.fromXDR(signed, this.passphrase);
|
|
831
|
+
if (toHex(envelope.hash()) !== txHash)
|
|
832
|
+
throw new OwnerError("signer changed the reviewed transaction body", contractId, fn);
|
|
662
833
|
let sent;
|
|
663
834
|
try {
|
|
664
835
|
sent = await this.server.sendTransaction(envelope);
|
|
@@ -686,21 +857,48 @@ export class SoranOwner {
|
|
|
686
857
|
}
|
|
687
858
|
async restore(sim, pub) {
|
|
688
859
|
const pre = sim.restorePreamble;
|
|
689
|
-
|
|
860
|
+
if (!/^(0|[1-9][0-9]*)$/.test(pre.minResourceFee))
|
|
861
|
+
throw new OwnerError("invalid restoration resource fee", null, "restore_footprint");
|
|
862
|
+
const quoted = BigInt(pre.minResourceFee);
|
|
863
|
+
const data = pre.transactionData.build();
|
|
864
|
+
const encoded = BigInt(data.resourceFee.toString());
|
|
865
|
+
if (encoded < 0n)
|
|
866
|
+
throw new OwnerError("invalid encoded restoration resource fee", null, "restore_footprint");
|
|
867
|
+
const resourceFee = quoted > encoded ? quoted : encoded;
|
|
868
|
+
this.assertNetworkFee((BigInt(this.fee) + resourceFee).toString(), "restore_footprint");
|
|
690
869
|
const tx = new TransactionBuilder(await this.sourceAccount(pub, "restore_footprint"), {
|
|
691
|
-
|
|
870
|
+
// SDK 17 adds Soroban resourceFee itself; supply the base only once.
|
|
871
|
+
fee: this.fee,
|
|
692
872
|
networkPassphrase: this.passphrase,
|
|
693
873
|
})
|
|
694
|
-
.setSorobanData(
|
|
874
|
+
.setSorobanData(new SorobanDataBuilder(data).setResourceFee(resourceFee.toString()).build())
|
|
695
875
|
.addOperation(Operation.restoreFootprint({}))
|
|
696
876
|
.setTimeout(this.timeoutSecs)
|
|
697
877
|
.build();
|
|
878
|
+
this.assertNetworkFee(tx.fee, "restore_footprint");
|
|
879
|
+
const txHash = toHex(tx.hash());
|
|
698
880
|
const signed = await this.signEnvelope(tx.toXDR());
|
|
699
|
-
const
|
|
700
|
-
if (
|
|
701
|
-
throw new OwnerError(
|
|
881
|
+
const envelope = TransactionBuilder.fromXDR(signed, this.passphrase);
|
|
882
|
+
if (toHex(envelope.hash()) !== txHash)
|
|
883
|
+
throw new OwnerError("signer changed the reviewed restoration body", null, "restore_footprint");
|
|
884
|
+
let sent;
|
|
885
|
+
try {
|
|
886
|
+
sent = await this.server.sendTransaction(envelope);
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
throw new OwnerError(`restoration submission is uncertain (${String(error)}); check ${txHash} before retrying`, null, "restore_footprint", null, null, txHash);
|
|
890
|
+
}
|
|
891
|
+
if (sent.status === "ERROR" || sent.status === "TRY_AGAIN_LATER") {
|
|
892
|
+
throw new OwnerError(`restore_footprint: submit rejected: ${JSON.stringify(sent.errorResult ?? sent.status)}`, null, "restore_footprint", null, null, txHash);
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
await this.confirm(txHash, "", "restore_footprint", {});
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
if (error instanceof OwnerError)
|
|
899
|
+
throw error;
|
|
900
|
+
throw new OwnerError(`restoration confirmation is uncertain (${String(error)}); check ${txHash} before retrying`, null, "restore_footprint", null, null, txHash);
|
|
702
901
|
}
|
|
703
|
-
await this.confirm(sent.hash, "", "restore_footprint", {});
|
|
704
902
|
}
|
|
705
903
|
// Poll past the tx time bound (timeoutSecs) so we never declare failure
|
|
706
904
|
// while the transaction is still valid and pending inclusion.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ClaimAllowlistContext = {
|
|
2
|
+
network: string;
|
|
3
|
+
registry: string;
|
|
4
|
+
namespace: string;
|
|
5
|
+
registrar: string;
|
|
6
|
+
};
|
|
7
|
+
export type ClaimAllowlistBundle = {
|
|
8
|
+
version: 1;
|
|
9
|
+
context: ClaimAllowlistContext;
|
|
10
|
+
root: string;
|
|
11
|
+
entries: Array<{
|
|
12
|
+
account: string;
|
|
13
|
+
leaf: string;
|
|
14
|
+
proof: string[];
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
export declare function claimAllowlistLeaf(context: ClaimAllowlistContext, account: string): string;
|
|
18
|
+
export declare function claimAllowlistParent(left: string, right: string): string;
|
|
19
|
+
/** A complete list is required to generate a replacement root. Keep/distribute the returned proof bundle. */
|
|
20
|
+
export declare function buildClaimAllowlist(context: ClaimAllowlistContext, accounts: readonly string[]): ClaimAllowlistBundle;
|
|
21
|
+
export declare function verifyClaimAllowlistProof(context: ClaimAllowlistContext, account: string, proof: readonly string[], root: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hash, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
import { NativeClaimError, address, hex, hex32, sc, unhex } from "./native-codec.js";
|
|
3
|
+
export function claimAllowlistLeaf(context, account) {
|
|
4
|
+
return hex(hash(xdr.ScVal.scvVec([sc.symbol("allow_v1"), sc.bytes(unhex(context.network)), sc.address(address(context.registry, "contract", "Registry")), sc.bytes(unhex(context.namespace)), sc.address(address(context.registrar, "contract", "Registrar")), sc.address(address(account, "account", "allowlisted claimant"))]).toXDR()));
|
|
5
|
+
}
|
|
6
|
+
export function claimAllowlistParent(left, right) { hex32(left, "left leaf"); hex32(right, "right leaf"); const [first, second] = left < right ? [left, right] : [right, left]; const bytes = new Uint8Array(65); bytes[0] = 1; bytes.set(unhex(first), 1); bytes.set(unhex(second), 33); return hex(hash(bytes)); }
|
|
7
|
+
/** A complete list is required to generate a replacement root. Keep/distribute the returned proof bundle. */
|
|
8
|
+
export function buildClaimAllowlist(context, accounts) {
|
|
9
|
+
if (!Array.isArray(accounts) || accounts.length === 0 || accounts.length > 65536)
|
|
10
|
+
throw new NativeClaimError("allowlist requires 1–65536 distinct G accounts");
|
|
11
|
+
const unique = new Set();
|
|
12
|
+
const entries = accounts.map(account => { address(account, "account", "allowlisted claimant"); if (unique.has(account))
|
|
13
|
+
throw new NativeClaimError("duplicate allowlist account"); unique.add(account); return { account, leaf: claimAllowlistLeaf(context, account), proof: [] }; }).sort((a, b) => a.leaf.localeCompare(b.leaf));
|
|
14
|
+
let layer = entries.map((entry, index) => ({ hash: entry.leaf, indices: [index] }));
|
|
15
|
+
while (layer.length > 1) {
|
|
16
|
+
const next = [];
|
|
17
|
+
for (let i = 0; i < layer.length; i += 2) {
|
|
18
|
+
const left = layer[i], right = layer[i + 1];
|
|
19
|
+
if (!right) {
|
|
20
|
+
next.push(left);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
for (const index of left.indices)
|
|
24
|
+
entries[index].proof.push(right.hash);
|
|
25
|
+
for (const index of right.indices)
|
|
26
|
+
entries[index].proof.push(left.hash);
|
|
27
|
+
next.push({ hash: claimAllowlistParent(left.hash, right.hash), indices: [...left.indices, ...right.indices] });
|
|
28
|
+
}
|
|
29
|
+
layer = next;
|
|
30
|
+
}
|
|
31
|
+
return { version: 1, context: { ...context }, root: layer[0].hash, entries };
|
|
32
|
+
}
|
|
33
|
+
export function verifyClaimAllowlistProof(context, account, proof, root) { hex32(root, "allowlist root"); if (!Array.isArray(proof) || proof.length > 32)
|
|
34
|
+
throw new NativeClaimError("allowlist proof exceeds 32 siblings"); return proof.reduce(claimAllowlistParent, claimAllowlistLeaf(context, account)) === root; }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Transaction, authorizeEntry, xdr } from "@stellar/stellar-sdk";
|
|
2
|
+
export type NativeInvocation = {
|
|
3
|
+
contract: string;
|
|
4
|
+
method: string;
|
|
5
|
+
args: xdr.ScVal[];
|
|
6
|
+
children?: NativeInvocation[];
|
|
7
|
+
};
|
|
8
|
+
/** Derive this plan locally from a validated intent, never from a remote envelope. */
|
|
9
|
+
export type NativeAuthorizationPlan = {
|
|
10
|
+
source: string;
|
|
11
|
+
contract: string;
|
|
12
|
+
method: string;
|
|
13
|
+
args: xdr.ScVal[];
|
|
14
|
+
sourceInvocation: NativeInvocation;
|
|
15
|
+
eligibility?: {
|
|
16
|
+
account: string;
|
|
17
|
+
invocation: NativeInvocation;
|
|
18
|
+
latestLedger: number;
|
|
19
|
+
maxExpirationLedger: number;
|
|
20
|
+
};
|
|
21
|
+
maxFeeStroops: bigint;
|
|
22
|
+
};
|
|
23
|
+
export declare function authorizedInvocation(value: NativeInvocation): xdr.SorobanAuthorizedInvocation;
|
|
24
|
+
export declare function validateEligibilityAuthorization(entry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, requireSigned?: boolean): void;
|
|
25
|
+
/** Native account signing callback; hardware/custody integration keeps keys outside the SDK. */
|
|
26
|
+
export type EligibilitySigner = Parameters<typeof authorizeEntry>[1];
|
|
27
|
+
/** Signs only the expected admission entry. This helper never signs a transaction. */
|
|
28
|
+
export declare function signEligibilityAuthorization(unsignedEntry: xdr.SorobanAuthorizationEntry, expected: NonNullable<NativeAuthorizationPlan["eligibility"]>, signer: EligibilitySigner, expirationLedger: number, networkPassphrase: string): Promise<xdr.SorobanAuthorizationEntry>;
|
|
29
|
+
/** Exact local intent and native role scope; does not relax ordinary SDK authorization. */
|
|
30
|
+
export declare function validateNativeTransaction(tx: Transaction, plan: NativeAuthorizationPlan, requireEligibilitySigned?: boolean): void;
|
|
31
|
+
export declare function assertSignedBodyUnchanged(prepared: Transaction, signedXdr: string, passphrase: string): Transaction;
|