@mysten/docs 0.1.33 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/llms-index.md +17 -4
- package/dist/sponsor/basic-usage.md +170 -0
- package/dist/sponsor/best-practices.md +163 -0
- package/dist/sponsor/index.md +114 -0
- package/dist/sponsor/llms-index.md +7 -0
- package/dist/sponsor/validators.md +207 -0
- package/dist/sui/cryptography/index.md +225 -0
- package/dist/sui/cryptography/keypairs.md +36 -143
- package/dist/sui/cryptography/signers/aws-kms.md +110 -0
- package/dist/sui/cryptography/signers/gcp-kms.md +91 -0
- package/dist/sui/cryptography/signers/index.md +78 -0
- package/dist/sui/cryptography/signers/ledger.md +76 -0
- package/dist/sui/cryptography/{multisig.md → signers/multisig.md} +2 -2
- package/dist/sui/cryptography/{passkey.md → signers/passkey.md} +1 -1
- package/dist/sui/cryptography/{webcrypto-signer.md → signers/webcrypto.md} +6 -6
- package/dist/sui/index.md +4 -5
- package/dist/sui/llms-index.md +9 -4
- package/dist/sui/migrations/sui-2.0/index.md +14 -1
- package/dist/sui/zklogin.md +2 -2
- package/dist/walrus/index.md +6 -0
- package/package.json +21 -21
package/dist/llms-index.md
CHANGED
|
@@ -81,6 +81,14 @@
|
|
|
81
81
|
- [App Integration](./slush-wallet/dapp.md): Connect your app to Slush Wallet using the Slush Wallet SDK and wallet standard.
|
|
82
82
|
- [Deep Linking](./slush-wallet/deep-linking.md): Configure deep links to open Slush Wallet directly to specific screens or actions.
|
|
83
83
|
|
|
84
|
+
## Sponsor
|
|
85
|
+
> Build gas sponsorship services with pluggable validation
|
|
86
|
+
|
|
87
|
+
- [Sponsor](./sponsor.md): Build gas sponsorship services with pluggable transaction validation
|
|
88
|
+
- [Basic Usage](./sponsor/basic-usage.md): Build a service that validates and executes user-signed sponsored bytes
|
|
89
|
+
- [Validation](./sponsor/validators.md): Compose built-in and custom validation analyzers for sponsorship policy
|
|
90
|
+
- [Best Practices](./sponsor/best-practices.md): Operate a Sui sponsor with clear policy, gas limits, and outcome handling
|
|
91
|
+
|
|
84
92
|
## Sui SDK
|
|
85
93
|
> TypeScript interfaces for Sui
|
|
86
94
|
|
|
@@ -96,10 +104,15 @@
|
|
|
96
104
|
- [Coins and Balances](./sui/transactions/coins-and-balances.md): Work with coin objects and address balances in transactions
|
|
97
105
|
- [Commands and Inputs Reference](./sui/transactions/reference.md): Complete reference for transaction commands and input types
|
|
98
106
|
- [Building Offline](./sui/transactions/offline.md): Build transactions without a network connection
|
|
99
|
-
- [
|
|
100
|
-
- [
|
|
101
|
-
- [
|
|
102
|
-
- [
|
|
107
|
+
- [Cryptography](./sui/cryptography.md): Sign and verify Sui transactions and messages with keypairs and external signers
|
|
108
|
+
- [Key pairs](./sui/cryptography/keypairs.md): Create Ed25519, Secp256k1, and Secp256r1 keypairs and derive them from mnemonics and secret keys
|
|
109
|
+
- [Signers](./sui/cryptography/signers.md): Signer implementations beyond keypairs — cloud KMS, Ledger, Web Crypto, passkeys, and multisig
|
|
110
|
+
- [AWS KMS Signer](./sui/cryptography/signers/aws-kms.md): Sign Sui transactions with a key stored in AWS Key Management Service
|
|
111
|
+
- [GCP KMS Signer](./sui/cryptography/signers/gcp-kms.md): Sign Sui transactions with a key stored in Google Cloud Key Management Service
|
|
112
|
+
- [Ledger Signer](./sui/cryptography/signers/ledger.md): Sign Sui transactions with a Ledger hardware wallet
|
|
113
|
+
- [Web Crypto Signer](./sui/cryptography/signers/webcrypto.md): Sign transactions using the Web Crypto API for secure browser-based key management
|
|
114
|
+
- [Passkey](./sui/cryptography/signers/passkey.md): Use WebAuthn passkeys for Sui transaction signing.
|
|
115
|
+
- [Multi-Signature Transactions](./sui/cryptography/signers/multisig.md): Create multi-signature transactions with multiple signers on Sui.
|
|
103
116
|
- [The `@mysten/sui/utils` package](./sui/utils.md): Utility functions for addresses, coins, and common operations in the Sui TypeScript SDK.
|
|
104
117
|
- [Derived Objects](./sui/utils/derived_objects.md): Compute derived object IDs from parent objects for deterministic offline derivation.
|
|
105
118
|
- [BCS](./sui/bcs.md): Binary Canonical Serialization for encoding Sui Move types.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Basic Usage
|
|
2
|
+
|
|
3
|
+
> Build a service that validates and executes user-signed sponsored bytes
|
|
4
|
+
|
|
5
|
+
In the examples below, `userSigner` is the user's `Signer`. In an app, the user signs through
|
|
6
|
+
dapp-kit's `signTransaction` instead.
|
|
7
|
+
|
|
8
|
+
## Recommended flow
|
|
9
|
+
|
|
10
|
+
Prefer services that expect **already-built bytes plus the user's signature**:
|
|
11
|
+
|
|
12
|
+
1. The client gets the sponsor address.
|
|
13
|
+
2. The client builds final transaction bytes with the sponsor as gas owner and address-balance gas.
|
|
14
|
+
3. The user signs those exact bytes.
|
|
15
|
+
4. The backend validates the bytes, co-signs, and executes.
|
|
16
|
+
|
|
17
|
+
This means the sponsor knows exactly what it is co-signing, there is no half-signed transaction
|
|
18
|
+
waiting on a user after the sponsor signs, and the flow avoids an extra sponsor-build round trip
|
|
19
|
+
before the user can sign.
|
|
20
|
+
|
|
21
|
+
## Provide the sponsor address
|
|
22
|
+
|
|
23
|
+
The client needs a sponsor address before it builds the transaction. For services where the sponsor
|
|
24
|
+
account might rotate, expose the current address through a small config endpoint:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
app.get('/config', (c) => c.json({ sponsor: sponsor.address }));
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Prefer fetching this value dynamically when you want the backend to rotate the sponsor key or move
|
|
31
|
+
sponsorship to a different account without forcing client deployments. Use the chosen address as the
|
|
32
|
+
transaction gas owner. In local examples where the client and sponsor live in the same process,
|
|
33
|
+
`sponsor.address` is the same value.
|
|
34
|
+
|
|
35
|
+
## Client builds the transaction
|
|
36
|
+
|
|
37
|
+
The client sets the **sender**, the **gas owner to the sponsor**, and the **gas payment to `[]`**
|
|
38
|
+
(address-balance gas). The user signs those final bytes and sends them to the sponsor service:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
|
|
42
|
+
const { sponsor } = await fetch('/config').then((res) => res.json());
|
|
43
|
+
|
|
44
|
+
transaction.setSender(userAddress);
|
|
45
|
+
transaction.setGasOwner(sponsor); // gas is paid by the sponsor…
|
|
46
|
+
transaction.setGasPayment([]); // …from its address balance (no specific gas coins)
|
|
47
|
+
const bytes = await transaction.build({ client });
|
|
48
|
+
|
|
49
|
+
const { signature: userSignature } = await userSigner.signTransaction(bytes);
|
|
50
|
+
|
|
51
|
+
await fetch('/sponsor', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'content-type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({ transaction: toBase64(bytes), userSignature }),
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The backend validates those bytes, co-signs, and executes:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const result = await sponsor.signAndExecuteTransaction({
|
|
62
|
+
transaction: body.transaction,
|
|
63
|
+
userSignature: body.userSignature,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
switch (result.$kind) {
|
|
67
|
+
case 'Rejected': // a validator declined; the sponsor never signed or executed
|
|
68
|
+
throw new Error(result.issues.map((issue) => issue.message).join('; '));
|
|
69
|
+
case 'FailedTransaction': // executed onchain but aborted, the sponsor still paid gas
|
|
70
|
+
throw new Error(`Transaction failed onchain: ${result.FailedTransaction.digest}`);
|
|
71
|
+
case 'Transaction': // executed successfully
|
|
72
|
+
return result.Transaction.digest;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`signAndExecuteTransaction` has **three** outcomes: a policy `Rejected` (never executed), a
|
|
77
|
+
`FailedTransaction` (executed but aborted onchain, where the sponsor still pays gas either way), and
|
|
78
|
+
a successful `Transaction`. The non-obvious one is `FailedTransaction`: a result that isn't
|
|
79
|
+
`Rejected` still isn't necessarily a success.
|
|
80
|
+
|
|
81
|
+
Pass `include` when the service needs extra transaction details in the response. The response always
|
|
82
|
+
includes effects because the service needs them to distinguish successful execution from failed
|
|
83
|
+
execution.
|
|
84
|
+
|
|
85
|
+
## A user transaction
|
|
86
|
+
|
|
87
|
+
A sponsored transaction operates on the **user's** objects, never the gas coin. When the user spends
|
|
88
|
+
SUI, source it from their own balance with `useGasCoin: false`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
function userPayment(amount: bigint) {
|
|
92
|
+
const tx = new Transaction();
|
|
93
|
+
const coin = tx.coin({ balance: amount, useGasCoin: false });
|
|
94
|
+
tx.transferObjects([coin], recipient);
|
|
95
|
+
return tx;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Over the network
|
|
100
|
+
|
|
101
|
+
The methods are transport-agnostic: they take transaction bytes as `Uint8Array` or base64 strings,
|
|
102
|
+
and user signatures as signature strings. You own the HTTP shape, so serialize binary fields as
|
|
103
|
+
base64. The client side is exactly the [build-and-sign flow above](#client-builds-the-transaction);
|
|
104
|
+
the backend maps each result `$kind` to a response. Every outcome is a `$kind`, so no try/catch is
|
|
105
|
+
needed for a policy rejection:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
// Server handler — wire it into your framework's route however you like.
|
|
109
|
+
async function handleSponsorRequest(body: { transaction: string; userSignature: string }) {
|
|
110
|
+
const result = await sponsor.signAndExecuteTransaction({
|
|
111
|
+
transaction: body.transaction,
|
|
112
|
+
userSignature: body.userSignature,
|
|
113
|
+
});
|
|
114
|
+
switch (result.$kind) {
|
|
115
|
+
case 'Rejected':
|
|
116
|
+
return { ok: false as const, issues: result.issues };
|
|
117
|
+
case 'FailedTransaction':
|
|
118
|
+
return { ok: false as const, issues: [{ message: 'Transaction failed on-chain.' }] };
|
|
119
|
+
case 'Transaction':
|
|
120
|
+
return { ok: true as const, digest: result.Transaction.digest };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Need per-request context, such as an auth token or tenant id? Validators read it from `options`, and
|
|
126
|
+
the sponsor requires it as typed `validationOptions`. See [Validation](/sponsor/validators). Handle
|
|
127
|
+
replay prevention and rate limiting in your service.
|
|
128
|
+
|
|
129
|
+
For a complete Hono server using this shape, see the
|
|
130
|
+
[sponsor server example](https://github.com/MystenLabs/ts-sdks/tree/main/packages/sponsor/examples/sponsor-server).
|
|
131
|
+
|
|
132
|
+
## Gas and unresolved transactions
|
|
133
|
+
|
|
134
|
+
When the client builds, the user already built and signed final bytes, so they must be resolved. The
|
|
135
|
+
sponsor never rebuilds them.
|
|
136
|
+
|
|
137
|
+
When the sponsor builds the transaction, what you pass to `signTransaction` can be **unresolved**,
|
|
138
|
+
with unresolved object inputs and no gas budget being fine. The sponsor sets itself as gas owner
|
|
139
|
+
with address-balance gas and calls `build({ client })`, which in one pass resolves object inputs,
|
|
140
|
+
sets the gas price and expiration, and (unless the transaction already pins one with
|
|
141
|
+
`tx.setGasBudget(…)`) **estimates the gas budget by dry-running**. Validation then runs on the
|
|
142
|
+
_built_ transaction, so `gasBudget({ max })` checks the resolved budget and analyzers see the exact
|
|
143
|
+
bytes that get signed.
|
|
144
|
+
|
|
145
|
+
## Advanced: sponsor builds the transaction
|
|
146
|
+
|
|
147
|
+
> **Warning:** For public sponsorship services, prefer accepting final bytes plus `userSignature`. Use the
|
|
148
|
+
> sponsor-builds flow only when the backend owns transaction construction and you understand the
|
|
149
|
+
> trust boundary.
|
|
150
|
+
|
|
151
|
+
Alternatively, hand the sponsor the user's commands. The sponsor sets itself as gas owner,
|
|
152
|
+
builds\n(address-balance gas, and a dry-run-estimated budget unless you pinned one), validates, and
|
|
153
|
+
signs. The user then signs the returned bytes, and both signatures execute together. `sender` is
|
|
154
|
+
optional, applied only when the transaction doesn't already set one (for example, bare commands):
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const result = await sponsor.signTransaction({
|
|
158
|
+
transaction,
|
|
159
|
+
sender: userAddress, // optional — only used if `transaction` has no sender
|
|
160
|
+
});
|
|
161
|
+
if (result.$kind === 'Rejected') {
|
|
162
|
+
throw new Error(result.issues.map((issue) => issue.message).join('; '));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { signature: userSignature } = await userSigner.signTransaction(result.bytes);
|
|
166
|
+
await client.core.executeTransaction({
|
|
167
|
+
transaction: result.bytes,
|
|
168
|
+
signatures: [userSignature, result.sponsorSignature],
|
|
169
|
+
});
|
|
170
|
+
```
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Best Practices
|
|
2
|
+
|
|
3
|
+
> Operate a Sui sponsor with clear policy, gas limits, and outcome handling
|
|
4
|
+
|
|
5
|
+
Gas sponsorship is a backend operating boundary. The user supplies transaction bytes; the sponsor
|
|
6
|
+
decides whether to pay for them. Treat every sponsored transaction as untrusted input until your
|
|
7
|
+
policy validates the exact bytes that get signed.
|
|
8
|
+
|
|
9
|
+
## Prefer user-signed transaction bytes
|
|
10
|
+
|
|
11
|
+
For public sponsor services, prefer accepting **already-built bytes plus `userSignature`**. The
|
|
12
|
+
client builds the transaction with the sponsor as gas owner, the user signs those final bytes, and
|
|
13
|
+
the sponsor validates, co-signs, and executes.
|
|
14
|
+
|
|
15
|
+
This keeps the safer trust boundary: the sponsor only signs after the user has signed the exact
|
|
16
|
+
transaction bytes. See [Basic Usage](/sponsor/basic-usage) for the recommended flow.
|
|
17
|
+
|
|
18
|
+
## Understand the Sui gas model
|
|
19
|
+
|
|
20
|
+
The Sponsor SDK uses address-balance gas:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
transaction.setGasOwner(sponsorAddress);
|
|
24
|
+
transaction.setGasPayment([]);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
An empty gas payment means the sponsor pays from its SUI address balance. This avoids locking
|
|
28
|
+
specific gas coin objects and allows parallel sponsor execution, but it also means the sponsor's
|
|
29
|
+
address balance is the operating balance at risk.
|
|
30
|
+
|
|
31
|
+
Fund the address balance intentionally. Faucet coins are coin objects; move SUI into the address
|
|
32
|
+
balance with a transaction such as `0x2::coin::send_funds`.
|
|
33
|
+
|
|
34
|
+
## Constrain sponsorship to product flows
|
|
35
|
+
|
|
36
|
+
Never expose an endpoint that blindly co-signs any user transaction. At minimum, constrain:
|
|
37
|
+
|
|
38
|
+
- **Allowed targets:** Use `allowedFunctions()` for fully qualified `package::module::function`
|
|
39
|
+
targets, not broad package allowlists, when the product flow only needs one function.
|
|
40
|
+
- **PTB templates:** Validate the full Programmable Transaction Block shape your product expects:
|
|
41
|
+
command count, command kinds, Move targets, type arguments, argument sources, object ownership,
|
|
42
|
+
and value movement. A function allowlist is a starting point, not a complete policy.
|
|
43
|
+
- **Gas budget:** Add `gasBudget({ max })` so a valid but expensive transaction cannot use the
|
|
44
|
+
protocol maximum.
|
|
45
|
+
- **Sender signature:** Add `userSignatureMatchesSender()` so the supplied signature matches the
|
|
46
|
+
transaction sender before the sponsor co-signs.
|
|
47
|
+
- **Value movement:** Use analyzers such as `balanceFlows` when sponsorship depends on payment,
|
|
48
|
+
refunds, or tenant-specific balances.
|
|
49
|
+
|
|
50
|
+
The
|
|
51
|
+
[sponsor server example](https://github.com/MystenLabs/ts-sdks/tree/main/packages/sponsor/examples/sponsor-server)
|
|
52
|
+
uses a basic PTB template: exactly one command, and that command must be a call to one configured
|
|
53
|
+
`package::module::function`. Real products should make the template as specific as their flow
|
|
54
|
+
requires.
|
|
55
|
+
|
|
56
|
+
## Keep `defaults()`
|
|
57
|
+
|
|
58
|
+
`defaults()` protects against common sponsorship mistakes:
|
|
59
|
+
|
|
60
|
+
| Validator | Why it matters |
|
|
61
|
+
| ------------------------- | ----------------------------------------------------------------------------- |
|
|
62
|
+
| `validSender()` | Rejects unset senders and transactions where the sender is the sponsor |
|
|
63
|
+
| `onlyAddressBalanceGas()` | Requires empty gas payment so users cannot nominate sponsor gas coin objects |
|
|
64
|
+
| `gasCoinNotUsed()` | Rejects commands that use `tx.gas`, which belongs to the sponsor |
|
|
65
|
+
| `onlySenderWithdrawals()` | Rejects address-balance withdrawals from the sponsor or any non-sender source |
|
|
66
|
+
| `simulationSucceeds()` | Rejects transactions that dry-run as aborting |
|
|
67
|
+
| `boundedExpiration()` | Rejects missing or too-far-ahead transaction expirations |
|
|
68
|
+
|
|
69
|
+
Once you pass `validate`, defaults no longer run automatically. Include `defaults()` explicitly:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const sponsor = createSponsor({
|
|
73
|
+
signer,
|
|
74
|
+
client,
|
|
75
|
+
validate: [defaults(), gasBudget({ max: 50_000_000n }), allowedFunctions([target])],
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Gas budget is not the whole economic risk
|
|
80
|
+
|
|
81
|
+
A gas cap limits how much gas the sponsor is willing to cover, but it is not a full business-policy
|
|
82
|
+
check.
|
|
83
|
+
|
|
84
|
+
Sui transactions can create, mutate, transfer, share, and delete objects. Storage costs and storage
|
|
85
|
+
rebates can create economic incentives that are not obvious from the gas budget alone. If you
|
|
86
|
+
sponsor arbitrary transactions, a user can try to externalize costs to the sponsor while directing
|
|
87
|
+
objects, balances, or rebate-related value to themselves or another address.
|
|
88
|
+
|
|
89
|
+
For product flows with financial value, validate the command shape and inspect net value movement:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
|
|
93
|
+
const USDC = '0x...::usdc::USDC';
|
|
94
|
+
|
|
95
|
+
const requirePayment = createAnalyzer({
|
|
96
|
+
dependencies: { balanceFlows: analyzers.balanceFlows },
|
|
97
|
+
analyze:
|
|
98
|
+
() =>
|
|
99
|
+
({ balanceFlows }) => {
|
|
100
|
+
const paidToSponsor =
|
|
101
|
+
balanceFlows.sponsor
|
|
102
|
+
?.filter((flow) => flow.coinType === USDC)
|
|
103
|
+
.reduce((sum, flow) => sum + flow.amount, 0n) ?? 0n;
|
|
104
|
+
|
|
105
|
+
return paidToSponsor >= 10_000n
|
|
106
|
+
? { result: null }
|
|
107
|
+
: { result: [{ code: 'UNDERPAID', message: 'Sponsor was not paid enough.' }] };
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Keep sponsor balances intentional
|
|
113
|
+
|
|
114
|
+
If the sponsor signer is loaded from an environment variable, treat it as a hot key. Keep only the
|
|
115
|
+
SUI needed for short operating windows in the sponsor address balance, and top it up from a more
|
|
116
|
+
protected account.
|
|
117
|
+
|
|
118
|
+
For production services, prefer a managed signer:
|
|
119
|
+
|
|
120
|
+
- Use a KMS-backed signer such as `@mysten/aws-kms-signer` or `@mysten/gcp-kms-signer`.
|
|
121
|
+
- Restrict who can deploy or read the signer configuration.
|
|
122
|
+
- Separate the sponsor hot account from treasury accounts.
|
|
123
|
+
- Rotate keys and drain old sponsor balances when access changes.
|
|
124
|
+
|
|
125
|
+
The Sponsor SDK accepts any `Signer`, so KMS signers can be used in place of an in-memory keypair.
|
|
126
|
+
|
|
127
|
+
## Dry-run is not an execution guarantee
|
|
128
|
+
|
|
129
|
+
`simulationSucceeds()` dry-runs the transaction before signing, but chain state can change before
|
|
130
|
+
execution. A transaction that passed simulation can still abort later and charge the sponsor gas.
|
|
131
|
+
|
|
132
|
+
Use dry-run as one policy input, not as the only control. Add:
|
|
133
|
+
|
|
134
|
+
- gas caps,
|
|
135
|
+
- target allowlists,
|
|
136
|
+
- command-shape checks,
|
|
137
|
+
- rate limits,
|
|
138
|
+
- authentication,
|
|
139
|
+
- per-user or per-tenant quotas.
|
|
140
|
+
|
|
141
|
+
Onchain replay isn't something you handle here. Sui executes a signed transaction at most once, and
|
|
142
|
+
`boundedExpiration()` (a default) supplies the bounded expiration it requires for address-balance
|
|
143
|
+
gas. Application-level idempotency is still yours: if a flow should be sponsored only once per user,
|
|
144
|
+
dedupe it with a request ID, nonce, or stored intent.
|
|
145
|
+
|
|
146
|
+
## Handle rejection and failed execution separately
|
|
147
|
+
|
|
148
|
+
A `Rejected` result means the sponsor did not sign or execute. A `FailedTransaction` means the
|
|
149
|
+
transaction executed onchain and failed, so the sponsor might have paid gas.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
switch (result.$kind) {
|
|
153
|
+
case 'Rejected':
|
|
154
|
+
return { ok: false, status: 403, issues: result.issues };
|
|
155
|
+
case 'FailedTransaction':
|
|
156
|
+
return { ok: false, status: 502, digest: result.FailedTransaction.digest };
|
|
157
|
+
case 'Transaction':
|
|
158
|
+
return { ok: true, digest: result.Transaction.digest };
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Do not collapse those outcomes into a single error. They have different accounting and retry
|
|
163
|
+
semantics.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Sponsor
|
|
2
|
+
|
|
3
|
+
> Build gas sponsorship services with pluggable transaction validation
|
|
4
|
+
|
|
5
|
+
> **Warning:** **Incubation package.** This package is still experimental and might change without notice. Pin
|
|
6
|
+
> versions and review changes before upgrading.
|
|
7
|
+
|
|
8
|
+
Primitives for building gas-sponsorship flows on Sui. You are the **sponsor operator**: you bring a
|
|
9
|
+
`Signer` (the sponsor) and a policy for what you'll pay for, and `createSponsor` takes a user's
|
|
10
|
+
transaction, fills in the gas, validates it against that policy, and adds the sponsor's signature.
|
|
11
|
+
The hard part, validating what you're sponsoring, is handled by a pluggable pipeline backed by the
|
|
12
|
+
[`@mysten/wallet-sdk` transaction analyzer](https://www.npmjs.com/package/@mysten/wallet-sdk).
|
|
13
|
+
|
|
14
|
+
The transaction comes from your user; you decide what's allowed and cover the gas.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```npm
|
|
19
|
+
npm i @mysten-incubation/sponsor @mysten/sui
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
|
|
26
|
+
const client = new SuiGrpcClient({
|
|
27
|
+
network: 'testnet',
|
|
28
|
+
baseUrl: 'https://fullnode.testnet.sui.io:443',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const sponsor = createSponsor({
|
|
32
|
+
signer: Ed25519Keypair.fromSecretKey(process.env.SPONSOR_KEY!),
|
|
33
|
+
client,
|
|
34
|
+
// `defaults()` keeps the baseline (see Defaults).
|
|
35
|
+
validate: [defaults(), gasBudget({ max: 50_000_000n }), allowedFunctions(['0xabc::shop::buy'])],
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## How gas works
|
|
40
|
+
|
|
41
|
+
The sponsor pays gas from its **address balance**. The empty gas payment (`setGasPayment([])`) draws
|
|
42
|
+
from the sponsor's onchain SUI balance rather than from specific gas coins. This means the sponsor
|
|
43
|
+
can sign and execute in parallel:
|
|
44
|
+
|
|
45
|
+
Address-balance gas doesn't lock specific coin objects, so concurrent sponsored transactions don't
|
|
46
|
+
contend for gas coins. A sender that reuses its own versioned objects (coins) across rapid
|
|
47
|
+
transactions still has to serialize those, but that's the sender's concern, not the sponsor's.
|
|
48
|
+
|
|
49
|
+
You fund the address balance by depositing SUI into it (for example, a one-time
|
|
50
|
+
`0x2::coin::send_funds(coin, sponsorAddress)`); the faucet only hands out coin objects, not address
|
|
51
|
+
balance.
|
|
52
|
+
|
|
53
|
+
Because gas is the sponsor's, **a sponsored transaction must never use the gas coin** (`tx.gas`).
|
|
54
|
+
`gasCoinNotUsed()` (a default) enforces this. When the user needs SUI, they spend their own with
|
|
55
|
+
`tx.coin({ balance, useGasCoin: false })`.
|
|
56
|
+
|
|
57
|
+
## Defaults
|
|
58
|
+
|
|
59
|
+
If you pass no `validate`, the sponsor runs `defaults()`: `validSender()`,
|
|
60
|
+
`onlyAddressBalanceGas()`, `gasCoinNotUsed()`, `onlySenderWithdrawals()`, `simulationSucceeds()`,
|
|
61
|
+
and `boundedExpiration()`. Once you add your own validators they no longer run automatically; drop
|
|
62
|
+
them back in as one entry (the `validate` array flattens nested arrays):
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
createSponsor({ signer, client }); // runs defaults()
|
|
66
|
+
createSponsor({ signer, client, validate: [defaults(), allowedFunctions(['0xabc::shop::buy'])] });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Each default guards something real: `validSender()` requires a sender and rejects transactions where
|
|
70
|
+
the sponsor is also the sender; `onlyAddressBalanceGas()` and `gasCoinNotUsed()` stop a caller from
|
|
71
|
+
spending the sponsor's gas (its address balance pays, and the gas coin is the sponsor's);
|
|
72
|
+
`onlySenderWithdrawals()` rejects any `FundsWithdrawal` input that isn't the sender's, including one
|
|
73
|
+
from the sponsor's **address balance** (the same balance that pays gas, a direct drain the gas-coin
|
|
74
|
+
check can't see, because the withdrawal is an input, not a command argument); `simulationSucceeds()`
|
|
75
|
+
avoids paying for a transaction that aborts; and `boundedExpiration()` requires a bounded expiration
|
|
76
|
+
for address-balance gas.
|
|
77
|
+
|
|
78
|
+
Two things the defaults **don't** do, by design (handle them at your service boundary):
|
|
79
|
+
|
|
80
|
+
- **No gas ceiling.** A legitimate but expensive transaction can cost up to the protocol max. Add
|
|
81
|
+
`gasBudget({ max })` for a cap, and rate-limit / authenticate callers.
|
|
82
|
+
- **No execution guarantee.** `simulationSucceeds()` is a dry-run; onchain state can shift before
|
|
83
|
+
execution, so a later-aborting sponsored transaction can still charge the sponsor gas.
|
|
84
|
+
|
|
85
|
+
Keep `defaults()` unless you're deliberately replacing those checks.
|
|
86
|
+
|
|
87
|
+
For **offline-only signing** (no dry-run), use validators that read only `data` (no
|
|
88
|
+
`transactionResponse`). Nothing depends on simulation, so the sponsor never simulates:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
createSponsor({ signer, client, validate: [validSender(), gasCoinNotUsed()] });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## What `transaction` is
|
|
95
|
+
|
|
96
|
+
The sponsor methods take a `transaction` that is anything `Transaction.from` accepts:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
type TransactionInput = Transaction | Uint8Array | string; // a Transaction, built bytes, base64, or JSON
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
When you also pass a `userSignature`, the bytes are already final (the user signed them), so that
|
|
103
|
+
form must be the exact bytes: a `Uint8Array` or a base64 string (not a `Transaction` or JSON). The
|
|
104
|
+
sponsor never rebuilds them (so the user's signature stays valid). The sender is already part of
|
|
105
|
+
those bytes, so there's no `sender` parameter in this flow. `userSignature` might be a single string
|
|
106
|
+
or an array (for example, multiple required signers).
|
|
107
|
+
|
|
108
|
+
## Next steps
|
|
109
|
+
|
|
110
|
+
- [Basic Usage](/sponsor/basic-usage) covers the recommended service flow: client-built bytes, user
|
|
111
|
+
signatures, network usage, and the advanced sponsor-builds flow.
|
|
112
|
+
- [Validation](/sponsor/validators) explains custom validation analyzers, request-scoped options,
|
|
113
|
+
analyzer execution, and built-in validation analyzers.
|
|
114
|
+
- [Best Practices](/sponsor/best-practices) adds policy, gas, and operational guidance.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Sponsor
|
|
2
|
+
> Build gas sponsorship services with pluggable validation
|
|
3
|
+
|
|
4
|
+
- [Sponsor](..md): Build gas sponsorship services with pluggable transaction validation
|
|
5
|
+
- [Basic Usage](./basic-usage.md): Build a service that validates and executes user-signed sponsored bytes
|
|
6
|
+
- [Validation](./validators.md): Compose built-in and custom validation analyzers for sponsorship policy
|
|
7
|
+
- [Best Practices](./best-practices.md): Operate a Sui sponsor with clear policy, gas limits, and outcome handling
|