@ember-finance/sdk 2.17.0 → 2.19.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.
@@ -0,0 +1,13 @@
1
+ import { VaultsApi } from "../api/v2/index.js";
2
+ /**
3
+ * The generated vaults client, on the fetch adapter so this module keeps one
4
+ * transport in the browser and in Node.
5
+ */
6
+ export declare function vaultsApi(baseUrl: string): VaultsApi;
7
+ /** Status and Retry-After of a failed call, or undefined when no response came back. */
8
+ export declare function httpFailure(error: unknown): {
9
+ status: number;
10
+ retryAfter?: string;
11
+ } | undefined;
12
+ /** True for a transport failure (no response), which is worth retrying. */
13
+ export declare function isNetworkError(error: unknown): boolean;
@@ -0,0 +1,26 @@
1
+ import axios from "axios";
2
+ import { Configuration, VaultsApi } from "../api/v2/index.js";
3
+ /**
4
+ * The generated vaults client, on the fetch adapter so this module keeps one
5
+ * transport in the browser and in Node.
6
+ */
7
+ export function vaultsApi(baseUrl) {
8
+ return new VaultsApi(new Configuration({
9
+ basePath: baseUrl.replace(/\/$/, ""),
10
+ baseOptions: { adapter: "fetch" }
11
+ }));
12
+ }
13
+ /** Status and Retry-After of a failed call, or undefined when no response came back. */
14
+ export function httpFailure(error) {
15
+ if (!axios.isAxiosError(error) || !error.response)
16
+ return undefined;
17
+ const retryAfter = error.response.headers?.["retry-after"];
18
+ return {
19
+ status: error.response.status,
20
+ retryAfter: typeof retryAfter === "string" ? retryAfter : undefined
21
+ };
22
+ }
23
+ /** True for a transport failure (no response), which is worth retrying. */
24
+ export function isNetworkError(error) {
25
+ return axios.isAxiosError(error) && !error.response && !axios.isCancel(error);
26
+ }
@@ -10,7 +10,6 @@ export interface HttpPointerSourceOptions {
10
10
  env?: PlatformEnv;
11
11
  /** Overrides `env` entirely. For a local deployment, or a proxy. */
12
12
  baseUrl?: string;
13
- path?: string;
14
13
  /** How long a coin's rows may be reused, in milliseconds. Default 30s. */
15
14
  cacheMs?: number;
16
15
  }
@@ -1,5 +1,6 @@
1
1
  import { environmentConfig } from "../common/constants.js";
2
- import { isAbortError, isTransientStatus, retryAfterMs } from "./batch.js";
2
+ import { httpFailure, isNetworkError, vaultsApi } from "./api.js";
3
+ import { isTransientStatus, retryAfterMs } from "./batch.js";
3
4
  import { coinTypesMatch, normalizeCoinType } from "./sui.js";
4
5
  /**
5
6
  * Pointers held in memory. Useful for tests, and for pinning a known venue set
@@ -33,7 +34,7 @@ export function httpPointerSource(options = {}) {
33
34
  // makes, not to cache across a session: pointers change when a venue is
34
35
  // added, and a long window would keep a new one invisible.
35
36
  const baseUrl = options.baseUrl ?? environmentConfig[options.env ?? "prod"].apiHost;
36
- const path = options.path ?? "/api/v2/vaults/placements";
37
+ const api = vaultsApi(baseUrl);
37
38
  const cacheMs = options.cacheMs ?? 30000;
38
39
  /**
39
40
  * One request for every coin. The endpoint answers with the whole registry
@@ -59,12 +60,7 @@ export function httpPointerSource(options = {}) {
59
60
  inFlight.set(key, { at: Date.now(), rows });
60
61
  return rows;
61
62
  };
62
- const request = (coin) => {
63
- const url = new URL(`${baseUrl.replace(/\/$/, "")}${path}`);
64
- if (coin !== undefined)
65
- url.searchParams.set("coin", coin);
66
- return fetchPlacements(url.toString());
67
- };
63
+ const request = (coin) => fetchPlacements(api, coin);
68
64
  const rowsFor = async (coin) => {
69
65
  if (Date.now() >= perCoinUntil) {
70
66
  try {
@@ -139,34 +135,31 @@ const FALLBACK_RECHECK_MS = 5 * 60000;
139
135
  * Retries transient failures. Every vault's venues now ride on this one
140
136
  * request, and callers keep a failed discover rather than retrying it.
141
137
  */
142
- async function fetchPlacements(url) {
138
+ async function fetchPlacements(api, coin) {
143
139
  for (let attempt = 1;; attempt++) {
144
140
  let waitMs;
145
141
  try {
146
- const response = await fetch(url, {
147
- headers: { Accept: "application/json" }
148
- });
149
- if (response.ok) {
150
- const body = (await response.json());
151
- return body.placements ?? [];
152
- }
153
- if (!isTransientStatus(response.status) || attempt >= FETCH_ATTEMPTS) {
154
- throw new PointerSourceError(response.status);
155
- }
156
- waitMs = retryAfterMs(response.headers?.get("retry-after"));
157
- // Retrying before the server asked would only add to the load it
158
- // is shedding and fail again.
159
- if (waitMs !== undefined && waitMs > RETRY_MAX_MS) {
160
- throw new PointerSourceError(response.status);
161
- }
142
+ const { data } = await api.getCoinPlacements(coin);
143
+ return data.placements ?? [];
162
144
  }
163
145
  catch (error) {
164
- // fetch rejects only on network failure; retry that, not our own
165
- // errors or an abort.
166
- if (error instanceof PointerSourceError ||
167
- isAbortError(error) ||
168
- attempt >= FETCH_ATTEMPTS) {
169
- throw error;
146
+ const failure = httpFailure(error);
147
+ if (!failure) {
148
+ // Only a transport failure is worth retrying, not an abort or a
149
+ // bug of our own.
150
+ if (!isNetworkError(error) || attempt >= FETCH_ATTEMPTS)
151
+ throw error;
152
+ }
153
+ else {
154
+ if (!isTransientStatus(failure.status) || attempt >= FETCH_ATTEMPTS) {
155
+ throw new PointerSourceError(failure.status);
156
+ }
157
+ waitMs = retryAfterMs(failure.retryAfter);
158
+ // Retrying before the server asked would only add to the load it
159
+ // is shedding and fail again.
160
+ if (waitMs !== undefined && waitMs > RETRY_MAX_MS) {
161
+ throw new PointerSourceError(failure.status);
162
+ }
170
163
  }
171
164
  }
172
165
  const backoff = Math.min(RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_MAX_MS);
@@ -1,4 +1,5 @@
1
1
  import { environmentConfig } from "../common/constants.js";
2
+ import { httpFailure, vaultsApi } from "./api.js";
2
3
  /**
3
4
  * Resolves a vault to the receipt coins it issues, from the vaults API.
4
5
  *
@@ -6,14 +7,17 @@ import { environmentConfig } from "../common/constants.js";
6
7
  * each, so this returns one entry per chain rather than assuming a single coin.
7
8
  */
8
9
  export function httpVaultCoinsResolver(options = {}) {
9
- const baseUrl = (options.baseUrl ?? environmentConfig[options.env ?? "prod"].apiHost).replace(/\/$/, "");
10
- const get = async (path) => {
11
- const response = await fetch(`${baseUrl}${path}`, {
12
- headers: { Accept: "application/json" }
13
- });
14
- if (!response.ok)
15
- throw new Error(`vaults api ${response.status} for ${path}`);
16
- return (await response.json());
10
+ const api = vaultsApi(options.baseUrl ?? environmentConfig[options.env ?? "prod"].apiHost);
11
+ const get = async (path, call) => {
12
+ try {
13
+ return (await call()).data;
14
+ }
15
+ catch (error) {
16
+ const failure = httpFailure(error);
17
+ if (failure)
18
+ throw new Error(`vaults api ${failure.status} for ${path}`);
19
+ throw error;
20
+ }
17
21
  };
18
22
  // The chain catalogue is what carries the EVM network id. Without it an EVM
19
23
  // coin has no chainId, and every read for it fails rather than returning a
@@ -22,7 +26,7 @@ export function httpVaultCoinsResolver(options = {}) {
22
26
  const chainRefs = () => {
23
27
  if (chains)
24
28
  return chains;
25
- chains = get("/api/v2/vaults/supported-chains").then(supported => {
29
+ chains = get("/api/v2/vaults/supported-chains", () => api.getSupportedChains()).then(supported => {
26
30
  const byIdentifier = new Map();
27
31
  for (const chain of supported) {
28
32
  if (!chain.identifier)
@@ -51,10 +55,10 @@ export function httpVaultCoinsResolver(options = {}) {
51
55
  return {
52
56
  async coinsForVault(vaultId) {
53
57
  const [vaults, catalogue] = await Promise.all([
54
- get(`/api/v2/vaults?vaultId=${encodeURIComponent(vaultId)}`),
58
+ get(`/api/v2/vaults?vaultId=${vaultId}`, () => api.getVaults(vaultId)),
55
59
  chainRefs()
56
60
  ]);
57
- const list = Array.isArray(vaults) ? vaults : [vaults];
61
+ const list = vaults;
58
62
  const vault = list.find(v => v.id === vaultId) ?? list[0];
59
63
  if (!vault)
60
64
  throw new Error(`vault ${vaultId} not found`);
@@ -56,11 +56,34 @@ export interface ITxBuilderOptionalParams {
56
56
  txBlock?: TransactionBlock;
57
57
  gasBudget?: number;
58
58
  sender?: string;
59
+ /**
60
+ * Build the call against this package id instead of the deployment's.
61
+ *
62
+ * Guarded entry points abort unless invoked through the package whose
63
+ * compiled VERSION equals `ProtocolConfig.version`, so during an upgrade —
64
+ * new package published, version not yet increased — the callable package is
65
+ * the retired one, which no on-chain record names.
66
+ */
67
+ packageId?: string;
59
68
  }
60
69
  export interface IVaultOptionalParams extends ITxBuilderOptionalParams {
61
70
  dryRun?: boolean;
62
71
  returnTxb?: boolean;
63
72
  }
73
+ /** `version` and the protocol-wide pause flag from the shared ProtocolConfig. */
74
+ export interface IProtocolConfigState {
75
+ /** The package VERSION the protocol currently accepts, as a decimal string. */
76
+ version: string;
77
+ pauseNonAdminOperations: boolean;
78
+ }
79
+ /** Whether one package id can service a guarded call right now. */
80
+ export interface IPackageSupportResult {
81
+ packageId: string;
82
+ /** True when this package's compiled VERSION equals `ProtocolConfig.version`. */
83
+ supported: boolean;
84
+ /** Abort or resolution reason when `supported` is false. */
85
+ reason?: string;
86
+ }
64
87
  export interface ICancellationPendingWithdrawalRequest {
65
88
  sequence_number: NumStr;
66
89
  timestamp: NumStr;
@@ -1,8 +1,8 @@
1
1
  import { BaseClient as SuiClient } from "@mysten/sui/client";
2
2
  import { OnChainCalls } from "./onchain-calls.js";
3
- import { ISuiDeployment, IVaultOptionalParams } from "../interfaces/index.js";
3
+ import { IPackageSupportResult, IProtocolConfigState, ISuiDeployment, IVaultOptionalParams } from "../interfaces/index.js";
4
4
  import { OnChainCallResponse, Signer, SuiAddress, SuiTransactionBlockResponseOptions } from "@firefly-exchange/library-sui";
5
- import { NumStr } from "@firefly-exchange/library-sui";
5
+ import { Address, NumStr } from "@firefly-exchange/library-sui";
6
6
  export declare class AdminCalls extends OnChainCalls {
7
7
  constructor(_network: string, _suiClient: SuiClient, _deployment: ISuiDeployment, _signer?: Signer, _walletAddress?: string);
8
8
  /**
@@ -73,6 +73,61 @@ export declare class AdminCalls extends OnChainCalls {
73
73
  * @returns OnChainCallResponse
74
74
  */
75
75
  pauseNonAdminOperations(pause: boolean, options?: IVaultOptionalParams & SuiTransactionBlockResponseOptions): Promise<OnChainCallResponse>;
76
+ /**
77
+ * Guardian fast-path for pausing/unpausing all non-admin protocol operations.
78
+ * Needs no AdminCap — the signer must be the address `set_guardian` recorded.
79
+ *
80
+ * Only executes through the package whose compiled VERSION matches
81
+ * `ProtocolConfig.version`; pass `options.packageId` (see
82
+ * {@link getSupportedPackageId}) to route through a retired one while an
83
+ * upgrade is half-applied.
84
+ * @param pause True to pause non admin operations, false to resume
85
+ * @param options Optional tx execution params
86
+ * @returns OnChainCallResponse
87
+ */
88
+ guardianPauseNonAdminOperations(pause: boolean, options?: IVaultOptionalParams & SuiTransactionBlockResponseOptions): Promise<OnChainCallResponse>;
89
+ /**
90
+ * Set or clear the guardian address. AdminCap-gated.
91
+ * @param newGuardian The new guardian address, or the zero address to unset
92
+ * @param options Optional tx execution params
93
+ * @returns OnChainCallResponse
94
+ */
95
+ setGuardian(newGuardian: Address, options?: IVaultOptionalParams & SuiTransactionBlockResponseOptions): Promise<OnChainCallResponse>;
96
+ /** Reader for the shared ProtocolConfig; rebuilt per call so a deployment update is picked up. */
97
+ private protocolConfigReader;
98
+ /**
99
+ * Read `version` and `pause_non_admin_operations` off the shared ProtocolConfig.
100
+ * @returns The package VERSION the protocol accepts, and the protocol-wide pause flag
101
+ */
102
+ getProtocolConfigState(): Promise<IProtocolConfigState>;
103
+ /**
104
+ * Read the guardian address. See {@link SuiProtocolConfigReader.getGuardian} —
105
+ * it is a dynamic field, so it never shows up in the ProtocolConfig's contents,
106
+ * and an absent field reads as the zero address.
107
+ * @returns The guardian address, or the zero address when unset
108
+ */
109
+ getGuardian(): Promise<Address>;
110
+ /**
111
+ * Ask the chain whether a package id is the one the protocol currently accepts,
112
+ * by simulating `admin::verify_supported_package` against it.
113
+ *
114
+ * The compiled VERSION has no getter, so running the assert is the only way to
115
+ * find out. Simulated with checks disabled, so it needs no gas and no signer.
116
+ * @param packageId Package id to test; defaults to the deployment's
117
+ * @returns Whether the package is supported, and the abort reason when it is not
118
+ */
119
+ isPackageSupported(packageId?: string): Promise<IPackageSupportResult>;
120
+ /**
121
+ * First of `candidatePackageIds` that the protocol currently accepts, probed in
122
+ * order. Callers pass the deployment's package id followed by retired ones, so
123
+ * a half-applied upgrade still resolves to something callable.
124
+ * @param candidatePackageIds Package ids to probe, most-preferred first
125
+ * @returns The supported package id (null if none) and every probe result
126
+ */
127
+ getSupportedPackageId(candidatePackageIds?: string[]): Promise<{
128
+ supportedPackageId: string | null;
129
+ probes: IPackageSupportResult[];
130
+ }>;
76
131
  /**
77
132
  * Increase the protocol version
78
133
  * @param options Optional tx execution params
@@ -1,5 +1,15 @@
1
1
  import { OnChainCalls } from "./onchain-calls.js";
2
+ import { SUI_ZERO_ADDRESS, SuiProtocolConfigReader } from "./protocol-config-reader.js";
2
3
  import { TransactionBlock } from "@firefly-exchange/library-sui";
4
+ /** Node resolution errors arrive percent-encoded. */
5
+ const decodeReason = (message) => {
6
+ try {
7
+ return decodeURIComponent(message);
8
+ }
9
+ catch {
10
+ return message;
11
+ }
12
+ };
3
13
  export class AdminCalls extends OnChainCalls {
4
14
  constructor(_network, _suiClient, _deployment, _signer, _walletAddress) {
5
15
  super(_network, _suiClient, _deployment, _signer, _walletAddress);
@@ -113,6 +123,116 @@ export class AdminCalls extends OnChainCalls {
113
123
  this.txBuilder.pauseNonAdminOperations(pause, { ...options, txBlock: txb });
114
124
  return this.execCall(txb, options);
115
125
  }
126
+ /**
127
+ * Guardian fast-path for pausing/unpausing all non-admin protocol operations.
128
+ * Needs no AdminCap — the signer must be the address `set_guardian` recorded.
129
+ *
130
+ * Only executes through the package whose compiled VERSION matches
131
+ * `ProtocolConfig.version`; pass `options.packageId` (see
132
+ * {@link getSupportedPackageId}) to route through a retired one while an
133
+ * upgrade is half-applied.
134
+ * @param pause True to pause non admin operations, false to resume
135
+ * @param options Optional tx execution params
136
+ * @returns OnChainCallResponse
137
+ */
138
+ async guardianPauseNonAdminOperations(pause, options) {
139
+ const txb = options?.txBlock || new TransactionBlock();
140
+ this.txBuilder.guardianPauseNonAdminOperations(pause, {
141
+ ...options,
142
+ txBlock: txb
143
+ });
144
+ return this.execCall(txb, options);
145
+ }
146
+ /**
147
+ * Set or clear the guardian address. AdminCap-gated.
148
+ * @param newGuardian The new guardian address, or the zero address to unset
149
+ * @param options Optional tx execution params
150
+ * @returns OnChainCallResponse
151
+ */
152
+ async setGuardian(newGuardian, options) {
153
+ const txb = options?.txBlock || new TransactionBlock();
154
+ this.txBuilder.setGuardian(newGuardian, { ...options, txBlock: txb });
155
+ return this.execCall(txb, options);
156
+ }
157
+ /** Reader for the shared ProtocolConfig; rebuilt per call so a deployment update is picked up. */
158
+ protocolConfigReader() {
159
+ return new SuiProtocolConfigReader(this.suiClient, this.parser.getProtocolConfig());
160
+ }
161
+ /**
162
+ * Read `version` and `pause_non_admin_operations` off the shared ProtocolConfig.
163
+ * @returns The package VERSION the protocol accepts, and the protocol-wide pause flag
164
+ */
165
+ async getProtocolConfigState() {
166
+ return this.protocolConfigReader().getProtocolConfigState();
167
+ }
168
+ /**
169
+ * Read the guardian address. See {@link SuiProtocolConfigReader.getGuardian} —
170
+ * it is a dynamic field, so it never shows up in the ProtocolConfig's contents,
171
+ * and an absent field reads as the zero address.
172
+ * @returns The guardian address, or the zero address when unset
173
+ */
174
+ async getGuardian() {
175
+ return this.protocolConfigReader().getGuardian();
176
+ }
177
+ /**
178
+ * Ask the chain whether a package id is the one the protocol currently accepts,
179
+ * by simulating `admin::verify_supported_package` against it.
180
+ *
181
+ * The compiled VERSION has no getter, so running the assert is the only way to
182
+ * find out. Simulated with checks disabled, so it needs no gas and no signer.
183
+ * @param packageId Package id to test; defaults to the deployment's
184
+ * @returns Whether the package is supported, and the abort reason when it is not
185
+ */
186
+ async isPackageSupported(packageId) {
187
+ const target = packageId || this.parser.getPackageId();
188
+ const txb = this.txBuilder.verifySupportedPackage({ packageId: target });
189
+ txb.setSender(SUI_ZERO_ADDRESS);
190
+ try {
191
+ const result = await this.suiClient.core.simulateTransaction({
192
+ transaction: txb,
193
+ checksEnabled: false
194
+ });
195
+ const simulated = result.Transaction ?? result.FailedTransaction;
196
+ const supported = result.$kind === "Transaction" &&
197
+ simulated?.status?.success === true;
198
+ return {
199
+ packageId: target,
200
+ supported,
201
+ reason: supported
202
+ ? undefined
203
+ : (simulated?.status?.error?.message ?? "unknown")
204
+ };
205
+ }
206
+ catch (error) {
207
+ // A package that predates the function fails at resolution rather than
208
+ // execution, and the node percent-encodes that message.
209
+ return {
210
+ packageId: target,
211
+ supported: false,
212
+ reason: decodeReason(error?.message ?? "unknown")
213
+ };
214
+ }
215
+ }
216
+ /**
217
+ * First of `candidatePackageIds` that the protocol currently accepts, probed in
218
+ * order. Callers pass the deployment's package id followed by retired ones, so
219
+ * a half-applied upgrade still resolves to something callable.
220
+ * @param candidatePackageIds Package ids to probe, most-preferred first
221
+ * @returns The supported package id (null if none) and every probe result
222
+ */
223
+ async getSupportedPackageId(candidatePackageIds) {
224
+ const candidates = candidatePackageIds && candidatePackageIds.length > 0
225
+ ? candidatePackageIds
226
+ : [this.parser.getPackageId()];
227
+ const probes = [];
228
+ for (const candidate of candidates) {
229
+ probes.push(await this.isPackageSupported(candidate));
230
+ }
231
+ return {
232
+ supportedPackageId: probes.find(p => p.supported)?.packageId ?? null,
233
+ probes
234
+ };
235
+ }
116
236
  /**
117
237
  * Increase the protocol version
118
238
  * @param options Optional tx execution params
@@ -5,3 +5,4 @@ export * from "./vault-admin.js";
5
5
  export * from "./operator.js";
6
6
  export * from "./vault.js";
7
7
  export * from "./atomic-liquidity.js";
8
+ export * from "./protocol-config-reader.js";
@@ -5,3 +5,4 @@ export * from "./vault-admin.js";
5
5
  export * from "./operator.js";
6
6
  export * from "./vault.js";
7
7
  export * from "./atomic-liquidity.js";
8
+ export * from "./protocol-config-reader.js";
@@ -0,0 +1,24 @@
1
+ import { BaseClient as SuiClient } from "@mysten/sui/client";
2
+ import { IProtocolConfigState } from "../interfaces/index.js";
3
+ /** The protocol's sentinel for "no guardian". */
4
+ export declare const SUI_ZERO_ADDRESS: string;
5
+ export declare class SuiProtocolConfigReader {
6
+ suiClient: SuiClient;
7
+ protocolConfigId: string;
8
+ constructor(_suiClient: SuiClient, _protocolConfigId: string);
9
+ /**
10
+ * Read `version` and `pause_non_admin_operations`.
11
+ * @returns The package VERSION the protocol accepts, and the protocol-wide pause flag
12
+ */
13
+ getProtocolConfigState(): Promise<IProtocolConfigState>;
14
+ /**
15
+ * Read the guardian address.
16
+ *
17
+ * The guardian is a dynamic field keyed by `b"guardian"`, not a struct field,
18
+ * so it never appears in the ProtocolConfig's contents. An absent field means
19
+ * the guardian was never set or has been cleared, which the protocol
20
+ * represents as the zero address.
21
+ * @returns The guardian address, or the zero address when unset
22
+ */
23
+ getGuardian(): Promise<string>;
24
+ }
@@ -0,0 +1,67 @@
1
+ import { bcs } from "@mysten/sui/bcs";
2
+ import { normalizeSuiAddress } from "@mysten/sui/utils";
3
+ /** Matches `KEY_GUARDIAN` in `admin.move`. */
4
+ const GUARDIAN_FIELD_KEY = "guardian";
5
+ /** The protocol's sentinel for "no guardian". */
6
+ export const SUI_ZERO_ADDRESS = normalizeSuiAddress("0x0");
7
+ /** gRPC flattens Move structs; JSON-RPC keeps a `fields` wrapper. */
8
+ const unwrapMoveFields = (value) => value?.fields ?? value;
9
+ export class SuiProtocolConfigReader {
10
+ constructor(_suiClient, _protocolConfigId) {
11
+ this.suiClient = _suiClient;
12
+ this.protocolConfigId = _protocolConfigId;
13
+ }
14
+ /**
15
+ * Read `version` and `pause_non_admin_operations`.
16
+ * @returns The package VERSION the protocol accepts, and the protocol-wide pause flag
17
+ */
18
+ async getProtocolConfigState() {
19
+ const res = await this.suiClient.core.getObject({
20
+ objectId: this.protocolConfigId,
21
+ include: { json: true }
22
+ });
23
+ const fields = unwrapMoveFields(res.object?.json);
24
+ if (!fields || fields.version === undefined) {
25
+ throw new Error(`ProtocolConfig ${this.protocolConfigId} not found`);
26
+ }
27
+ return {
28
+ version: String(fields.version),
29
+ pauseNonAdminOperations: Boolean(fields.pause_non_admin_operations)
30
+ };
31
+ }
32
+ /**
33
+ * Read the guardian address.
34
+ *
35
+ * The guardian is a dynamic field keyed by `b"guardian"`, not a struct field,
36
+ * so it never appears in the ProtocolConfig's contents. An absent field means
37
+ * the guardian was never set or has been cleared, which the protocol
38
+ * represents as the zero address.
39
+ * @returns The guardian address, or the zero address when unset
40
+ */
41
+ async getGuardian() {
42
+ try {
43
+ const df = await this.suiClient.core.getDynamicField({
44
+ parentId: this.protocolConfigId,
45
+ name: {
46
+ type: "vector<u8>",
47
+ bcs: bcs
48
+ .vector(bcs.u8())
49
+ .serialize(new TextEncoder().encode(GUARDIAN_FIELD_KEY))
50
+ .toBytes()
51
+ }
52
+ });
53
+ const fieldId = df?.dynamicField?.fieldId;
54
+ if (!fieldId)
55
+ return SUI_ZERO_ADDRESS;
56
+ const obj = await this.suiClient.core.getObject({
57
+ objectId: fieldId,
58
+ include: { json: true }
59
+ });
60
+ const value = unwrapMoveFields(obj.object?.json)?.value;
61
+ return value ? normalizeSuiAddress(String(value)) : SUI_ZERO_ADDRESS;
62
+ }
63
+ catch {
64
+ return SUI_ZERO_ADDRESS;
65
+ }
66
+ }
67
+ }
@@ -1,6 +1,6 @@
1
1
  import { SuiDeploymentParser } from "../utils/index.js";
2
2
  import { ISuiDeployment, ITxBuilderOptionalParams } from "../interfaces/index.js";
3
- import { NumStr, TransactionBlock } from "@firefly-exchange/library-sui";
3
+ import { Address, NumStr, TransactionBlock } from "@firefly-exchange/library-sui";
4
4
  export declare class TxBuilder {
5
5
  parser: SuiDeploymentParser;
6
6
  constructor(_deployment: ISuiDeployment);
@@ -9,6 +9,13 @@ export declare class TxBuilder {
9
9
  * @param newDeployment The new deployment configuration
10
10
  */
11
11
  updateDeployment(newDeployment: ISuiDeployment): void;
12
+ /**
13
+ * Package id to build the call against: the caller's override when given,
14
+ * otherwise the deployment's. An override exists because a guarded call only
15
+ * executes through the package whose compiled VERSION matches
16
+ * `ProtocolConfig.version`, which is not always the newest one.
17
+ */
18
+ protected targetPackageId(options?: ITxBuilderOptionalParams): string;
12
19
  /**
13
20
  * Pauses or unpauses the non-admin operations
14
21
  * @param pause True if the non-admin operations should be paused, false otherwise
@@ -16,6 +23,40 @@ export declare class TxBuilder {
16
23
  * @returns TransactionBlock
17
24
  */
18
25
  pauseNonAdminOperations(pause: boolean, options?: ITxBuilderOptionalParams): TransactionBlock;
26
+ /**
27
+ * Guardian fast-path for pausing/unpausing all non-admin protocol operations.
28
+ * Callable directly by the guardian address — no AdminCap — and so the lever
29
+ * used during an incident.
30
+ *
31
+ * Aborts with EUnsupportedPackage (1000) unless it is invoked through the
32
+ * package whose compiled VERSION matches `ProtocolConfig.version`; pass
33
+ * `options.packageId` to route through a retired package while an upgrade is
34
+ * half-applied. See {@link verifySupportedPackage}.
35
+ * @param pause True to pause non admin operations, false to resume
36
+ * @param options Optional tx building params
37
+ * @returns TransactionBlock
38
+ */
39
+ guardianPauseNonAdminOperations(pause: boolean, options?: ITxBuilderOptionalParams): TransactionBlock;
40
+ /**
41
+ * Sets or clears the guardian address. AdminCap-gated. Pass the zero address
42
+ * to unset, which makes every guardian-gated entry point unreachable.
43
+ * @param newGuardian The new guardian address, or `0x0` to unset
44
+ * @param options Optional tx building params
45
+ * @returns TransactionBlock
46
+ */
47
+ setGuardian(newGuardian: Address, options?: ITxBuilderOptionalParams): TransactionBlock;
48
+ /**
49
+ * Asserts `ProtocolConfig.version` equals the VERSION compiled into the target
50
+ * package, aborting EUnsupportedPackage (1000) otherwise.
51
+ *
52
+ * On its own this changes nothing; it is built to be simulated. The compiled
53
+ * VERSION has no getter, so running this assert is the only way to ask whether
54
+ * a given package id is the one the protocol currently accepts — and it is the
55
+ * same check every guarded entry point performs first.
56
+ * @param options Optional tx building params; `packageId` selects the package to test
57
+ * @returns TransactionBlock
58
+ */
59
+ verifySupportedPackage(options?: ITxBuilderOptionalParams): TransactionBlock;
19
60
  /**
20
61
  * Increases the version of the protocol supported. Only admin can invoke this
21
62
  * @param options Optional tx building params
@@ -12,6 +12,15 @@ export class TxBuilder {
12
12
  updateDeployment(newDeployment) {
13
13
  this.parser.updateDeployment(newDeployment);
14
14
  }
15
+ /**
16
+ * Package id to build the call against: the caller's override when given,
17
+ * otherwise the deployment's. An override exists because a guarded call only
18
+ * executes through the package whose compiled VERSION matches
19
+ * `ProtocolConfig.version`, which is not always the newest one.
20
+ */
21
+ targetPackageId(options) {
22
+ return options?.packageId || this.parser.getPackageId();
23
+ }
15
24
  /**
16
25
  * Pauses or unpauses the non-admin operations
17
26
  * @param pause True if the non-admin operations should be paused, false otherwise
@@ -34,6 +43,80 @@ export class TxBuilder {
34
43
  txb.setSenderIfNotSet(options.sender);
35
44
  return txb;
36
45
  }
46
+ /**
47
+ * Guardian fast-path for pausing/unpausing all non-admin protocol operations.
48
+ * Callable directly by the guardian address — no AdminCap — and so the lever
49
+ * used during an incident.
50
+ *
51
+ * Aborts with EUnsupportedPackage (1000) unless it is invoked through the
52
+ * package whose compiled VERSION matches `ProtocolConfig.version`; pass
53
+ * `options.packageId` to route through a retired package while an upgrade is
54
+ * half-applied. See {@link verifySupportedPackage}.
55
+ * @param pause True to pause non admin operations, false to resume
56
+ * @param options Optional tx building params
57
+ * @returns TransactionBlock
58
+ */
59
+ guardianPauseNonAdminOperations(pause, options) {
60
+ const txb = options?.txBlock || new TransactionBlock();
61
+ txb.moveCall({
62
+ arguments: [
63
+ txb.object(this.parser.getProtocolConfig()),
64
+ txb.pure.bool(pause)
65
+ ],
66
+ target: `${this.targetPackageId(options)}::gateway::guardian_pause_non_admin_operations`
67
+ });
68
+ if (options?.gasBudget)
69
+ txb.setGasBudget(options.gasBudget);
70
+ if (options?.sender)
71
+ txb.setSenderIfNotSet(options.sender);
72
+ return txb;
73
+ }
74
+ /**
75
+ * Sets or clears the guardian address. AdminCap-gated. Pass the zero address
76
+ * to unset, which makes every guardian-gated entry point unreachable.
77
+ * @param newGuardian The new guardian address, or `0x0` to unset
78
+ * @param options Optional tx building params
79
+ * @returns TransactionBlock
80
+ */
81
+ setGuardian(newGuardian, options) {
82
+ const txb = options?.txBlock || new TransactionBlock();
83
+ txb.moveCall({
84
+ arguments: [
85
+ txb.object(this.parser.getProtocolConfig()),
86
+ txb.object(this.parser.getAdminCap()),
87
+ txb.pure.address(newGuardian)
88
+ ],
89
+ target: `${this.targetPackageId(options)}::gateway::set_guardian`
90
+ });
91
+ if (options?.gasBudget)
92
+ txb.setGasBudget(options.gasBudget);
93
+ if (options?.sender)
94
+ txb.setSenderIfNotSet(options.sender);
95
+ return txb;
96
+ }
97
+ /**
98
+ * Asserts `ProtocolConfig.version` equals the VERSION compiled into the target
99
+ * package, aborting EUnsupportedPackage (1000) otherwise.
100
+ *
101
+ * On its own this changes nothing; it is built to be simulated. The compiled
102
+ * VERSION has no getter, so running this assert is the only way to ask whether
103
+ * a given package id is the one the protocol currently accepts — and it is the
104
+ * same check every guarded entry point performs first.
105
+ * @param options Optional tx building params; `packageId` selects the package to test
106
+ * @returns TransactionBlock
107
+ */
108
+ verifySupportedPackage(options) {
109
+ const txb = options?.txBlock || new TransactionBlock();
110
+ txb.moveCall({
111
+ arguments: [txb.object(this.parser.getProtocolConfig())],
112
+ target: `${this.targetPackageId(options)}::admin::verify_supported_package`
113
+ });
114
+ if (options?.gasBudget)
115
+ txb.setGasBudget(options.gasBudget);
116
+ if (options?.sender)
117
+ txb.setSenderIfNotSet(options.sender);
118
+ return txb;
119
+ }
37
120
  /**
38
121
  * Increases the version of the protocol supported. Only admin can invoke this
39
122
  * @param options Optional tx building params
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ember-finance/sdk",
3
3
  "description": "Ember Protocol SDK",
4
- "version": "2.17.0",
4
+ "version": "2.19.0",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
7
7
  "main": "./dist/index.js",