@quaivault/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/dist/abi/index.cjs +3349 -0
- package/dist/abi/index.cjs.map +1 -0
- package/dist/abi/index.d.cts +21 -0
- package/dist/abi/index.d.ts +21 -0
- package/dist/abi/index.js +23 -0
- package/dist/abi/index.js.map +1 -0
- package/dist/chunk-Y3PVKON6.js +3334 -0
- package/dist/chunk-Y3PVKON6.js.map +1 -0
- package/dist/index.cjs +7884 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2271 -0
- package/dist/index.d.ts +2271 -0
- package/dist/index.js +4549 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QuaiVault
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# @quaivault/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for [QuaiVault](https://quaivault.org) multisig vaults on Quai Network.
|
|
4
|
+
|
|
5
|
+
Reads go through the indexer when it is fresh and the chain when it is not; writes always
|
|
6
|
+
re-validate on chain before signing. Works in Node and the browser, with any signer.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @quaivault/sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
**Documentation:** [quaivault.org/docs/sdk](https://quaivault.org/docs/sdk) —
|
|
13
|
+
[guides](https://quaivault.org/docs/sdk-guides) ·
|
|
14
|
+
[API reference](https://quaivault.org/docs/sdk-reference)
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { connect } from '@quaivault/sdk';
|
|
20
|
+
|
|
21
|
+
// Read-only. No wallet, no key.
|
|
22
|
+
const qv = connect({ network: 'mainnet' });
|
|
23
|
+
|
|
24
|
+
const vault = qv.vault('0x005f2629A632962f4944d23686efDa5c160d535b');
|
|
25
|
+
const info = await vault.info();
|
|
26
|
+
console.log(`${info.threshold}-of-${info.owners.length}, balance ${info.balance}`);
|
|
27
|
+
|
|
28
|
+
for (const tx of await vault.pendingTransactions()) {
|
|
29
|
+
console.log(tx.status, `${tx.approvalCount}/${tx.threshold}`, tx.summary);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Add a signer to write:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const qv = connect({ network: 'mainnet', privateKey: process.env.QUAIVAULT_PRIVATE_KEY });
|
|
37
|
+
|
|
38
|
+
const { txHash } = await qv.vault(address).propose.transfer({
|
|
39
|
+
to: '0x00…',
|
|
40
|
+
amount: 1_000000000000000000n, // 1 QUAI
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
Everything is configurable by environment variable, explicit option, or network preset.
|
|
47
|
+
**Explicit options win, then the environment, then the preset.** Nothing is read at import
|
|
48
|
+
time, so importing the SDK never touches `process.env`.
|
|
49
|
+
|
|
50
|
+
See [`.env.example`](./.env.example) for the full list. The essentials:
|
|
51
|
+
|
|
52
|
+
| Variable | Purpose |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `QUAIVAULT_NETWORK` | `mainnet` or `testnet` |
|
|
55
|
+
| `QUAIVAULT_RPC_URL` | Quai RPC base URL (shard path appended automatically) |
|
|
56
|
+
| `QUAIVAULT_PRIVATE_KEY` | Signing key — required only for writes |
|
|
57
|
+
| `QUAIVAULT_INDEXER_ANON_KEY` | Override the shipped read-only indexer key |
|
|
58
|
+
| `QUAIVAULT_CONSISTENCY` | `auto` (default), `indexed`, or `chain` |
|
|
59
|
+
|
|
60
|
+
Networks:
|
|
61
|
+
|
|
62
|
+
| Preset | Chain ID | RPC | Indexer schema |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `mainnet` | 9 | `https://rpc.quai.network` | `mainnet` |
|
|
65
|
+
| `testnet` (Orchard) | 15000 | `https://orchard.rpc.quai.network` | `testnet` |
|
|
66
|
+
|
|
67
|
+
Indexer credentials ship with the SDK, so discovery, history and realtime work with no
|
|
68
|
+
configuration. The publishable key is a public credential — the indexer's RLS grants `anon`
|
|
69
|
+
`SELECT` and nothing else, with every write reserved for `service_role`. Override it only
|
|
70
|
+
to point at a self-hosted indexer or if it is rotated ahead of a release.
|
|
71
|
+
|
|
72
|
+
## Three things worth knowing
|
|
73
|
+
|
|
74
|
+
### 1. A successful receipt does not mean the transaction executed
|
|
75
|
+
|
|
76
|
+
QuaiVault has three paths where the Quai transaction succeeds but the vault transaction did
|
|
77
|
+
not run. `execute()` returns a discriminated result rather than a receipt:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const result = await vault.execute(txHash);
|
|
81
|
+
|
|
82
|
+
switch (result.outcome) {
|
|
83
|
+
case 'executed': break; // the target call succeeded
|
|
84
|
+
case 'failed': result.decodedRevert; // target reverted; TERMINAL
|
|
85
|
+
case 'timelock_started': result.executableAfter; // clock started, retry later
|
|
86
|
+
case 'approved_only': result.approvalsNeeded; // needs more signatures
|
|
87
|
+
}
|
|
88
|
+
console.log(result.message); // plain-language explanation
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **`failed`** — the vault marks the transaction executed *permanently* and emits
|
|
92
|
+
`TransactionFailed` instead of reverting. It cannot be retried; propose a replacement.
|
|
93
|
+
- **`timelock_started`** — a timelocked transaction that reached quorum without
|
|
94
|
+
`approvedAt` being set (e.g. the threshold was lowered mid-flight) has its clock started
|
|
95
|
+
by the first `execute` call, which then returns without executing.
|
|
96
|
+
- **`approved_only`** — `approveAndExecute` returned `false`, which a receipt cannot show.
|
|
97
|
+
|
|
98
|
+
### 2. Deploying requires mining a CREATE2 salt
|
|
99
|
+
|
|
100
|
+
Quai addresses are shard-scoped, so a vault must land on the deployer's shard or it is
|
|
101
|
+
unreachable. `create()` mines a salt automatically:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const { address, salt, predictionMatched } = await qv.factory.create(
|
|
105
|
+
{ owners: [a, b, c], threshold: 2, minExecutionDelay: 86_400 },
|
|
106
|
+
{ onProgress: (p) => console.log(p.step, p.message) },
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The predicted address is a pure function of `(factory, implementation, deployer, salt,
|
|
111
|
+
create params)` — including `initialModules` and `initialDelegatecallTargets`. Mining with
|
|
112
|
+
different params than you deploy with predicts the wrong address, so pass the real ones.
|
|
113
|
+
|
|
114
|
+
To mine ahead of time and deploy later, keep the salt and reuse it with identical params:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const mined = await qv.factory.mineSalt(params);
|
|
118
|
+
await qv.factory.create({ ...params, salt: mined.salt });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3. Indexed approval counts can over-report
|
|
122
|
+
|
|
123
|
+
The contract invalidates an owner's in-flight approvals when that owner is removed
|
|
124
|
+
(approval epochs). The indexer marks a confirmation inactive only on an explicit
|
|
125
|
+
`ApprovalRevoked`, so its stored `confirmation_count` is too high after any owner removal.
|
|
126
|
+
|
|
127
|
+
The SDK intersects confirmations with the current owner set to reproduce the contract's
|
|
128
|
+
count, and every write path re-reads the chain before signing. A stale count never gates a
|
|
129
|
+
signature.
|
|
130
|
+
|
|
131
|
+
## Knowing what you can do
|
|
132
|
+
|
|
133
|
+
`affordances()` answers "what may this address legally do to this transaction right now,
|
|
134
|
+
and if not now, when" — derived from the contract's actual rules, so you can plan instead
|
|
135
|
+
of discovering constraints through reverts.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
for (const a of await vault.affordances(txHash, caller)) {
|
|
139
|
+
console.log(a.allowed ? 'YES' : 'no', a.action, a.reason, a.availableAt);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
It encodes the subtle ones: proposer-cancel is permanently blocked once quorum is reached
|
|
144
|
+
(even if approvals are later revoked), self-calls bypass the timelock, and `expire` is
|
|
145
|
+
permissionless once past the deadline.
|
|
146
|
+
|
|
147
|
+
`describe(txHash)` renders the same information as a compact human-readable block.
|
|
148
|
+
|
|
149
|
+
## API
|
|
150
|
+
|
|
151
|
+
### Client
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
connect(options?): QuaiVaultClient
|
|
155
|
+
qv.vault(address): Vault
|
|
156
|
+
qv.vaults.forOwner(address) / forGuardian(address) / exists(address)
|
|
157
|
+
qv.factory.create(params, options?) / mineSalt(params) / predictAddress(deployer, salt, params)
|
|
158
|
+
qv.factory.implementation() / verify() / vaultCount() / vaultAt(i) / register(vault)
|
|
159
|
+
qv.indexerHealth()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Vault reads
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
vault.info() / owners() / threshold() / balance() / modules() / delegatecallTargets()
|
|
166
|
+
vault.isOwner(a) / isModuleEnabled(m) / isDelegatecallAllowed(t) / isValidSignature(hash)
|
|
167
|
+
vault.transaction(txHash) / pendingTransactions(page?) / transactionHistory(page?)
|
|
168
|
+
vault.transactionHash(to, value, data, nonce?)
|
|
169
|
+
vault.affordances(txHash, caller?) / describe(txHash, caller?)
|
|
170
|
+
vault.balances(opts?) / deposits(page?) / tokenTransfers(page?) / signedMessages()
|
|
171
|
+
vault.waitForExecutable(txHash, opts?)
|
|
172
|
+
vault.watch(handler, opts?) // Supabase Realtime
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Social recovery
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
vault.recovery.config() / isGuardian(a) / isEnabled() / hasPending()
|
|
179
|
+
vault.recovery.get(hash) / pending() / history(page?) / approvals(hash)
|
|
180
|
+
vault.recovery.predictHash(newOwners, newThreshold)
|
|
181
|
+
vault.recovery.initiate({ newOwners, newThreshold }) // guardians only
|
|
182
|
+
vault.recovery.approve(h) / revokeApproval(h) / execute(h) / cancel(h) / expire(h)
|
|
183
|
+
vault.recovery.affordances(hash, caller?)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Configuring guardians goes through the owner multisig
|
|
187
|
+
(`vault.propose.setupRecovery({ guardians, threshold, recoveryPeriodSeconds })`); everything
|
|
188
|
+
after that is guardian-driven. `cancel` is callable by any current vault owner — the owners'
|
|
189
|
+
defence against a hostile or mistaken guardian action.
|
|
190
|
+
|
|
191
|
+
Recovery status is derived from timestamps rather than read from a stored flag. Nothing
|
|
192
|
+
transitions a recovery to expired on its own: `expireRecovery` is a permissionless cleanup
|
|
193
|
+
call somebody has to make, so an un-cleaned recovery still reads as `pending` in the
|
|
194
|
+
indexer long after its deadline. `execute()` also pre-checks the transient owner count,
|
|
195
|
+
because new owners are added before old ones are removed and a fully disjoint replacement
|
|
196
|
+
can momentarily exceed the 20-owner cap.
|
|
197
|
+
|
|
198
|
+
### Vault writes
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
vault.propose.call / transfer / erc20Transfer / erc721Transfer / erc1155Transfer / batch
|
|
202
|
+
vault.propose.addOwner / removeOwner / changeThreshold / setMinExecutionDelay
|
|
203
|
+
vault.propose.enableModule / disableModule
|
|
204
|
+
vault.propose.addDelegatecallTarget / removeDelegatecallTarget
|
|
205
|
+
vault.propose.cancelByConsensus / signMessage / approveHashForEip1271 / setupRecovery
|
|
206
|
+
|
|
207
|
+
vault.approve(h) / approveAndExecute(h) / execute(h) / revokeApproval(h) / cancel(h) / expire(h)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`waitForExecutable` polls the chain until a transaction is `ready`, and fails fast rather
|
|
211
|
+
than spinning when waiting cannot help — below quorum, or already terminal.
|
|
212
|
+
|
|
213
|
+
Every `propose.*` accepts `{ expiration, executionDelay, dryRun }`. With `dryRun: true` you
|
|
214
|
+
get the encoded calldata, a gas estimate, and any predicted revert — without signing.
|
|
215
|
+
|
|
216
|
+
Preconditions are checked client-side against the contract's real rules, so failures name
|
|
217
|
+
the fix. Removing an owner below the threshold, batching without MultiSendCallOnly
|
|
218
|
+
whitelisted, or executing a `disableModule` proposal whose module list has since changed all
|
|
219
|
+
raise typed errors rather than reverting on chain.
|
|
220
|
+
|
|
221
|
+
### Errors
|
|
222
|
+
|
|
223
|
+
All errors extend `QuaiVaultError` with a stable `code`, a `remediation` string, and
|
|
224
|
+
`toJSON()`. Reverts are decoded against every QuaiVault ABI — 73 custom error selectors —
|
|
225
|
+
and rendered with guidance:
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
NotEnoughApprovals — The approval threshold has not been reached yet.
|
|
229
|
+
DelegateCallNotAllowed(0x00…) — DelegateCall targets must be explicitly whitelisted.
|
|
230
|
+
Propose addDelegatecallTarget for this address first.
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Balances
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const { native, tokens } = await vault.balances();
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Token discovery is indexer-driven — the chain cannot answer "which contracts has this
|
|
240
|
+
address touched" without scanning every log. Amounts are then verified on chain by default,
|
|
241
|
+
because replaying transfers misses anything the indexer did not observe. Pass
|
|
242
|
+
`{ verify: false }` to skip the reads.
|
|
243
|
+
|
|
244
|
+
### Waiting for the indexer
|
|
245
|
+
|
|
246
|
+
A transaction confirmed at block N is not queryable until the indexer reaches N. Proposing
|
|
247
|
+
and then immediately listing will silently miss it:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const { txHash, chainTxHash } = await vault.propose.transfer({ to, amount });
|
|
251
|
+
|
|
252
|
+
await vault.waitForIndexer(); // or waitForIndexer(blockNumber)
|
|
253
|
+
const pending = await vault.pendingTransactions(); // now includes it
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Returns `{ reached, lastIndexedBlock }` rather than throwing on timeout — a lagging indexer
|
|
257
|
+
is an expected operating condition, and the caller may prefer to fall back to a chain read.
|
|
258
|
+
With no argument it targets the current head of the vault's own Quai zone, derived from its
|
|
259
|
+
address (there is no single chain head on a sharded network).
|
|
260
|
+
|
|
261
|
+
### Realtime
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const sub = vault.watch((e) => console.log(e.topic, e.type), {
|
|
265
|
+
topics: ['transactions', 'confirmations', 'owners'],
|
|
266
|
+
});
|
|
267
|
+
await sub.unsubscribe();
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
All topics share one channel per vault. Events fire after the *indexer* processes a block,
|
|
271
|
+
so treat them as a signal to re-read rather than as state themselves.
|
|
272
|
+
|
|
273
|
+
### Standalone utilities
|
|
274
|
+
|
|
275
|
+
Usable without a client:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import {
|
|
279
|
+
predictVaultAddress, mineSalt, shardPrefixOf, // CREATE2
|
|
280
|
+
selfCall, tokenCalls, encodeMultiSend, // encoding
|
|
281
|
+
decodeCall, decodeMultiSendPayload, // decoding
|
|
282
|
+
classifyExecution, deriveStatus, deriveRecoveryStatus, computeAffordances,
|
|
283
|
+
decodeRevert, knownErrorSelectors,
|
|
284
|
+
} from '@quaivault/sdk';
|
|
285
|
+
|
|
286
|
+
import { QuaiVaultAbi, QuaiVaultFactoryAbi } from '@quaivault/sdk/abi';
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Quai addresses only
|
|
290
|
+
|
|
291
|
+
Quai has two ledgers. **Qi is a UTXO ledger with no contract execution**, so a Qi address
|
|
292
|
+
can never sign a vault transaction, approve a recovery, or receive value that stays on the
|
|
293
|
+
Quai side. The contracts do not check for this — `_addOwner` rejects only the zero address,
|
|
294
|
+
the vault itself, and the module sentinel — so a Qi owner is dead weight against the
|
|
295
|
+
threshold, and enough of them brick the vault permanently.
|
|
296
|
+
|
|
297
|
+
Two independent properties are encoded in an address, and both must hold:
|
|
298
|
+
|
|
299
|
+
| Property | Where | Valid |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| Zone | first byte | `0x00`–`0x02`, `0x10`–`0x12`, `0x20`–`0x22` |
|
|
302
|
+
| Ledger | 9th bit (high bit of the second byte) | clear = Quai, set = Qi |
|
|
303
|
+
|
|
304
|
+
They are orthogonal: `0x0081…` is in a real zone but is Qi, and `0x7E11…` is Quai-ledger
|
|
305
|
+
but in no zone. The SDK rejects both wherever an address is committed to a role or receives
|
|
306
|
+
value — owners, guardians, recovery owners, modules, delegatecall targets, transfer
|
|
307
|
+
recipients, the signing key, and the vault address itself.
|
|
308
|
+
|
|
309
|
+
Removals stay permissive on purpose: a vault that admitted a bad address before this check
|
|
310
|
+
existed must still be able to remove it.
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { assertQuaiAddress, isUsableQuaiAddress, inspectAddress } from '@quaivault/sdk';
|
|
314
|
+
|
|
315
|
+
isUsableQuaiAddress('0x0081…'); // false — Qi ledger
|
|
316
|
+
inspectAddress('0x0081…'); // { valid: false, zone: '0x00', ledger: 'qi', reason: … }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Resilience
|
|
320
|
+
|
|
321
|
+
Reads retry transient RPC and indexer failures with exponential backoff and full jitter:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
const qv = connect({
|
|
325
|
+
network: 'mainnet',
|
|
326
|
+
retry: { maxAttempts: 4, baseDelayMs: 250, onRetry: ({ attempt, error }) => log(attempt, error) },
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Writes are never retried.** A resubmit that looks like a timeout to the client may already
|
|
331
|
+
be in the mempool, so retrying risks a double broadcast. Only reads go through the policy.
|
|
332
|
+
|
|
333
|
+
The classifier errs toward *not* retrying: reverts (`CALL_EXCEPTION`), user rejections,
|
|
334
|
+
nonce and funds errors are permanent by definition, and anything unrecognised is treated as
|
|
335
|
+
permanent too — so a genuine bug surfaces immediately instead of hiding behind three slow
|
|
336
|
+
attempts. Rate limits, 5xx and raw transport failures are retried.
|
|
337
|
+
|
|
338
|
+
## Development
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
npm run sync-abis # regenerate src/abi from quaivault-contracts artifacts
|
|
342
|
+
npm run sync-abis -- --check # CI drift check
|
|
343
|
+
npm run typecheck
|
|
344
|
+
npm test
|
|
345
|
+
npm run build
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
ABIs are generated from `quaivault-contracts/artifacts`. The SDK is the single distribution
|
|
349
|
+
point — do not hand-edit `src/abi/*.json`.
|
|
350
|
+
|
|
351
|
+
## Publishing
|
|
352
|
+
|
|
353
|
+
The package is `@quaivault/sdk` — scoped, so `publishConfig.access` is set to `public`
|
|
354
|
+
(scoped packages default to restricted and would otherwise fail to publish).
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npm run sync-abis -- --check # fail if ABIs drifted from quaivault-contracts
|
|
358
|
+
npm run typecheck
|
|
359
|
+
npm test
|
|
360
|
+
npm run build
|
|
361
|
+
npm pack # inspect the tarball before publishing
|
|
362
|
+
npm publish
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`prepublishOnly` runs the first four automatically, so `npm publish` cannot ship a build
|
|
366
|
+
that fails a check or a stale ABI.
|
|
367
|
+
|
|
368
|
+
Releases go out through **npm trusted publishing**: push a `v*` tag and the release
|
|
369
|
+
workflow exchanges a GitHub OIDC token for a short-lived, workflow-scoped npm credential.
|
|
370
|
+
There is no `NPM_TOKEN` to leak or rotate, and provenance attestations are generated
|
|
371
|
+
automatically because the repository is public.
|
|
372
|
+
|
|
373
|
+
The trusted publisher is configured in the package's settings on npmjs.com and names
|
|
374
|
+
`release.yml` exactly. npm does not validate that configuration when it is saved, so a
|
|
375
|
+
mismatch in the org, repo or workflow filename only surfaces as a failed publish.
|
|
376
|
+
|
|
377
|
+
`npm publish` from a workstation still works and is what bootstrapped `0.1.0` — a package
|
|
378
|
+
must exist before a trusted publisher can be attached to it.
|
|
379
|
+
|
|
380
|
+
**Version pinning.** `quais` is tracked with `~1.0.0-alpha.53` rather than `^`. It is still
|
|
381
|
+
pre-1.0, so a caret range would allow 1.x minors that may change the API before release;
|
|
382
|
+
the tilde keeps consumers on the alpha line this SDK is verified against (alpha.53 and
|
|
383
|
+
alpha.55).
|
|
384
|
+
|
|
385
|
+
## License
|
|
386
|
+
|
|
387
|
+
MIT
|