@degen-insurance/sdk 0.2.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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/abi.d.ts +418 -0
- package/dist/abi.js +49 -0
- package/dist/alerts/state.d.ts +52 -0
- package/dist/alerts/state.js +56 -0
- package/dist/deployment.d.ts +59 -0
- package/dist/deployment.js +46 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +99 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/limits.d.ts +135 -0
- package/dist/limits.js +180 -0
- package/dist/telegram/render.d.ts +14 -0
- package/dist/telegram/render.js +44 -0
- package/dist/tx/build.d.ts +51 -0
- package/dist/tx/build.js +92 -0
- package/package.json +41 -0
package/dist/abi.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { parseAbi } from "viem";
|
|
2
|
+
export const universalRouterAbi = parseAbi([
|
|
3
|
+
"function buyWithCover(address token, uint256 minOut, uint8 tier) payable returns (uint256 received, uint256 policyId)",
|
|
4
|
+
"function buyWithCoverVia(address token, uint256 minOut, uint8 tier, address referrer) payable returns (uint256 received, uint256 policyId)",
|
|
5
|
+
"function buy(address token, uint256 minOut) payable returns (uint256 received)",
|
|
6
|
+
"function previewCover(uint8 tier, uint256 totalValue) view returns (uint256 notional, uint256 premium)",
|
|
7
|
+
]);
|
|
8
|
+
export const insuranceCoreAbi = parseAbi([
|
|
9
|
+
"function latchTrigger(address token) returns (bool)",
|
|
10
|
+
"function claim(uint256 id) returns (uint256)",
|
|
11
|
+
"function latchAndClaim(uint256 id) returns (uint256)",
|
|
12
|
+
"function remainingCapacity(address token) view returns (uint256)",
|
|
13
|
+
// The two ways out of an insured position. Both end the cover, because the core holds the
|
|
14
|
+
// position in escrow for the life of the policy -- a holder cannot simply sell it, and a
|
|
15
|
+
// partner with no exit to offer leaves users stuck until expiry.
|
|
16
|
+
"function sellAndVoid(uint256 id, uint256 minOut) returns (uint256)",
|
|
17
|
+
"function voidAndRelease(uint256 id) returns (uint256)",
|
|
18
|
+
"function policyCount() view returns (uint256)",
|
|
19
|
+
"function policies(uint256 id) view returns ((address,uint96,address,uint48,uint48,uint128,uint128,uint128,uint128,uint8,uint8,uint16))",
|
|
20
|
+
"function quote(address token, uint256 notional, uint256 tokensOut, uint8 tier) view returns (uint256 premium, uint256 payout, uint256 entryPrice, uint256 floorPrice)",
|
|
21
|
+
// Both share caps are cumulative, so headroom depends on the wallet and on every policy
|
|
22
|
+
// already open against the token. Read, never assumed.
|
|
23
|
+
"function walletTokensInsured(address token, address holder) view returns (uint256)",
|
|
24
|
+
"function tokenTokensInsured(address token) view returns (uint256)",
|
|
25
|
+
]);
|
|
26
|
+
export const riskRegistryAbi = parseAbi([
|
|
27
|
+
"function checkToken(address token) view returns (bool ok, uint8 reason)",
|
|
28
|
+
"function tiers(uint256 id) view returns ((uint16,uint16,uint16,bool))",
|
|
29
|
+
// Field order mirrors RiskRegistry.Params exactly. Six uint16s, not five -- getting that
|
|
30
|
+
// wrong shifts every uint128 after it and silently reads a bps value as a wei limit.
|
|
31
|
+
"function getParams() view returns ((uint32,uint32,uint32,uint16,uint16,uint16,uint16,uint16,uint16,uint128,uint128,uint128,uint128))",
|
|
32
|
+
]);
|
|
33
|
+
export const launchPoolAbi = parseAbi([
|
|
34
|
+
"function quoteReserve() view returns (uint256)",
|
|
35
|
+
"function tokenReserve() view returns (uint256)",
|
|
36
|
+
"function feeBps() view returns (uint16)",
|
|
37
|
+
"function launchPrice() view returns (uint256)",
|
|
38
|
+
"function spotPrice() view returns (uint256)",
|
|
39
|
+
]);
|
|
40
|
+
export const erc20Abi = parseAbi([
|
|
41
|
+
"function balanceOf(address) view returns (uint256)",
|
|
42
|
+
"function totalSupply() view returns (uint256)",
|
|
43
|
+
"function symbol() view returns (string)",
|
|
44
|
+
"function decimals() view returns (uint8)",
|
|
45
|
+
]);
|
|
46
|
+
export const partnerRegistryAbi = parseAbi([
|
|
47
|
+
"function outstanding(address partner) view returns (uint256)",
|
|
48
|
+
"function claim(address to) returns (uint256)",
|
|
49
|
+
]);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alerting for cover.
|
|
3
|
+
*
|
|
4
|
+
* For insurance specifically this is the most valuable thing a bot can offer, and it needs no
|
|
5
|
+
* custody at all: knowing your window is about to close, or that your claim just paid, is worth
|
|
6
|
+
* more than being able to buy from inside the chat.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Voided is new, and it is not a failure state: it is how a holder leaves early. The core
|
|
10
|
+
* escrows the position for the life of the policy, so exiting means `sellAndVoid` or
|
|
11
|
+
* `voidAndRelease`, and either ends the cover.
|
|
12
|
+
*/
|
|
13
|
+
export type PolicyStatus = "active" | "claimed" | "expired" | "voided";
|
|
14
|
+
export interface PolicyView {
|
|
15
|
+
id: string;
|
|
16
|
+
symbol: string;
|
|
17
|
+
token: string;
|
|
18
|
+
holder: string;
|
|
19
|
+
notionalWei: bigint;
|
|
20
|
+
maxPayoutWei: bigint;
|
|
21
|
+
openedAt: number;
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
status: PolicyStatus;
|
|
24
|
+
/** A trigger has been recorded inside this policy's window. */
|
|
25
|
+
triggered: boolean;
|
|
26
|
+
}
|
|
27
|
+
export type AlertKind = "cover_active" | "triggered_claimable" | "window_closing" | "claim_paid" | "expired_unused" | "cover_ended";
|
|
28
|
+
export interface Alert {
|
|
29
|
+
kind: AlertKind;
|
|
30
|
+
policyId: string;
|
|
31
|
+
symbol: string;
|
|
32
|
+
/** Deduplication key: one alert of each kind per policy, ever. */
|
|
33
|
+
key: string;
|
|
34
|
+
}
|
|
35
|
+
export interface AlertConfig {
|
|
36
|
+
/** How long before expiry to warn, in seconds. */
|
|
37
|
+
closingWindowSeconds: number;
|
|
38
|
+
}
|
|
39
|
+
export declare const DEFAULT_ALERT_CONFIG: AlertConfig;
|
|
40
|
+
/**
|
|
41
|
+
* Decide which alerts a policy warrants right now.
|
|
42
|
+
*
|
|
43
|
+
* `sent` carries the keys already delivered. Alerting is idempotent because the alternative is
|
|
44
|
+
* a bot that repeats "your window is closing" on every poll, which trains people to mute it and
|
|
45
|
+
* then miss the one message that mattered.
|
|
46
|
+
*/
|
|
47
|
+
export declare function alertsFor(policy: PolicyView, nowSeconds: number, sent: ReadonlySet<string>, config?: AlertConfig): Alert[];
|
|
48
|
+
/** Run the whole book, returning alerts to send and the updated sent-key set. */
|
|
49
|
+
export declare function sweep(policies: readonly PolicyView[], nowSeconds: number, sent: ReadonlySet<string>, config?: AlertConfig): {
|
|
50
|
+
alerts: Alert[];
|
|
51
|
+
sent: Set<string>;
|
|
52
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alerting for cover.
|
|
3
|
+
*
|
|
4
|
+
* For insurance specifically this is the most valuable thing a bot can offer, and it needs no
|
|
5
|
+
* custody at all: knowing your window is about to close, or that your claim just paid, is worth
|
|
6
|
+
* more than being able to buy from inside the chat.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_ALERT_CONFIG = { closingWindowSeconds: 180 };
|
|
9
|
+
/**
|
|
10
|
+
* Decide which alerts a policy warrants right now.
|
|
11
|
+
*
|
|
12
|
+
* `sent` carries the keys already delivered. Alerting is idempotent because the alternative is
|
|
13
|
+
* a bot that repeats "your window is closing" on every poll, which trains people to mute it and
|
|
14
|
+
* then miss the one message that mattered.
|
|
15
|
+
*/
|
|
16
|
+
export function alertsFor(policy, nowSeconds, sent, config = DEFAULT_ALERT_CONFIG) {
|
|
17
|
+
const out = [];
|
|
18
|
+
const push = (kind) => {
|
|
19
|
+
const key = `${policy.id}:${kind}`;
|
|
20
|
+
if (!sent.has(key))
|
|
21
|
+
out.push({ kind, policyId: policy.id, symbol: policy.symbol, key });
|
|
22
|
+
};
|
|
23
|
+
if (policy.status === "claimed") {
|
|
24
|
+
push("claim_paid");
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
if (policy.status === "expired") {
|
|
28
|
+
push("expired_unused");
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
if (policy.status === "voided") {
|
|
32
|
+
push("cover_ended");
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
push("cover_active");
|
|
36
|
+
if (policy.triggered) {
|
|
37
|
+
push("triggered_claimable");
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
const remaining = policy.expiresAt - nowSeconds;
|
|
41
|
+
if (remaining > 0 && remaining <= config.closingWindowSeconds)
|
|
42
|
+
push("window_closing");
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** Run the whole book, returning alerts to send and the updated sent-key set. */
|
|
46
|
+
export function sweep(policies, nowSeconds, sent, config = DEFAULT_ALERT_CONFIG) {
|
|
47
|
+
const next = new Set(sent);
|
|
48
|
+
const alerts = [];
|
|
49
|
+
for (const p of policies) {
|
|
50
|
+
for (const a of alertsFor(p, nowSeconds, next, config)) {
|
|
51
|
+
alerts.push(a);
|
|
52
|
+
next.add(a.key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { alerts, sent: next };
|
|
56
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Address } from "viem";
|
|
2
|
+
/**
|
|
3
|
+
* One deployment of the protocol, on one chain.
|
|
4
|
+
*
|
|
5
|
+
* This is the answer to "how do I switch chains?" and the reason there is no `setChain()`.
|
|
6
|
+
* A setter implies one current chain, which is fine for a browser tab and wrong for the thing
|
|
7
|
+
* this SDK is actually for: a bot serving many users at once. Two requests for different
|
|
8
|
+
* chains arriving together would race on the same global, and the loser would build calldata
|
|
9
|
+
* for one chain and send it to another. Passing the deployment makes that unrepresentable --
|
|
10
|
+
* two chains is two values, and they cannot interfere.
|
|
11
|
+
*
|
|
12
|
+
* It also exists to keep addresses that must agree from drifting apart. Passing `core`,
|
|
13
|
+
* `router` and `registry` as three loose arguments lets a caller mix one chain's router with
|
|
14
|
+
* another's core, and nothing would notice until the transaction reverted for reasons that
|
|
15
|
+
* look nothing like the cause.
|
|
16
|
+
*/
|
|
17
|
+
export interface Deployment {
|
|
18
|
+
/** EIP-155 chain id. Checked against the connected client by `assertChain`. */
|
|
19
|
+
chainId: number;
|
|
20
|
+
/** For error messages. Never used to decide anything. */
|
|
21
|
+
name: string;
|
|
22
|
+
core: Address;
|
|
23
|
+
router: Address;
|
|
24
|
+
registry: Address;
|
|
25
|
+
partnerRegistry?: Address;
|
|
26
|
+
/**
|
|
27
|
+
* Whether a calibration run has produced a rate card for this chain.
|
|
28
|
+
*
|
|
29
|
+
* `Chains.card()` reverts `NoRateCard` for an uncalibrated chain rather than guessing, and
|
|
30
|
+
* this SDK follows it: premiums are the deliverable of a calibration run and belong to the
|
|
31
|
+
* chain they were measured on. Selling cover at another chain's numbers is not a smaller
|
|
32
|
+
* version of being right.
|
|
33
|
+
*/
|
|
34
|
+
calibrated: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Robinhood Chain testnet.
|
|
38
|
+
*
|
|
39
|
+
* The only deployment exported here, because it is the only one whose addresses have been
|
|
40
|
+
* checked against the chain. Robinhood mainnet (4663) has a deployment in `broadcast/`, but
|
|
41
|
+
* shipping addresses nobody has verified is how a partner ends up pointed at the wrong
|
|
42
|
+
* contracts -- define it yourself with `defineDeployment` once you have confirmed it.
|
|
43
|
+
*/
|
|
44
|
+
export declare const ROBINHOOD_TESTNET: Deployment;
|
|
45
|
+
/** Define a deployment this SDK does not ship. Validates shape, not chain state. */
|
|
46
|
+
export declare function defineDeployment(d: Deployment): Deployment;
|
|
47
|
+
/** The slice of a viem public client this module needs. */
|
|
48
|
+
export interface ChainReader {
|
|
49
|
+
getChainId(): Promise<number>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Confirm the client is actually on the deployment's chain.
|
|
53
|
+
*
|
|
54
|
+
* Worth a round trip before signing anything. A client pointed at the wrong RPC produces
|
|
55
|
+
* calldata that is perfectly well formed and addressed to contracts that are not there, and
|
|
56
|
+
* the failure arrives as an unrelated-looking revert -- or worse, as a success against
|
|
57
|
+
* whatever else happens to live at that address.
|
|
58
|
+
*/
|
|
59
|
+
export declare function assertChain(client: ChainReader, deployment: Deployment): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robinhood Chain testnet.
|
|
3
|
+
*
|
|
4
|
+
* The only deployment exported here, because it is the only one whose addresses have been
|
|
5
|
+
* checked against the chain. Robinhood mainnet (4663) has a deployment in `broadcast/`, but
|
|
6
|
+
* shipping addresses nobody has verified is how a partner ends up pointed at the wrong
|
|
7
|
+
* contracts -- define it yourself with `defineDeployment` once you have confirmed it.
|
|
8
|
+
*/
|
|
9
|
+
export const ROBINHOOD_TESTNET = {
|
|
10
|
+
chainId: 46630,
|
|
11
|
+
name: "Robinhood Chain Testnet",
|
|
12
|
+
core: "0x38C22152fB1214ff73ef95FC26d4f9FDf7178D07",
|
|
13
|
+
router: "0xDbd1F2b5b27Fc4a5d2354c6aa1a46db9888DCE89",
|
|
14
|
+
registry: "0x9C9FBE362EaC60fe57Bd66303De8de36c9b8E467",
|
|
15
|
+
partnerRegistry: "0xe8E49a8574a267DAe6E0C54E31Ca24F0e1258718",
|
|
16
|
+
// The testnet card is Robinhood mainnet's, borrowed to have something to quote. It has not
|
|
17
|
+
// been calibrated against testnet flow, and there is no reason to think it would match.
|
|
18
|
+
calibrated: false,
|
|
19
|
+
};
|
|
20
|
+
/** Define a deployment this SDK does not ship. Validates shape, not chain state. */
|
|
21
|
+
export function defineDeployment(d) {
|
|
22
|
+
if (!Number.isInteger(d.chainId) || d.chainId <= 0)
|
|
23
|
+
throw new Error("chainId must be a positive integer");
|
|
24
|
+
for (const k of ["core", "router", "registry"]) {
|
|
25
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(d[k]))
|
|
26
|
+
throw new Error(`${k} is not an address`);
|
|
27
|
+
}
|
|
28
|
+
if (d.partnerRegistry && !/^0x[0-9a-fA-F]{40}$/.test(d.partnerRegistry)) {
|
|
29
|
+
throw new Error("partnerRegistry is not an address");
|
|
30
|
+
}
|
|
31
|
+
return d;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Confirm the client is actually on the deployment's chain.
|
|
35
|
+
*
|
|
36
|
+
* Worth a round trip before signing anything. A client pointed at the wrong RPC produces
|
|
37
|
+
* calldata that is perfectly well formed and addressed to contracts that are not there, and
|
|
38
|
+
* the failure arrives as an unrelated-looking revert -- or worse, as a success against
|
|
39
|
+
* whatever else happens to live at that address.
|
|
40
|
+
*/
|
|
41
|
+
export async function assertChain(client, deployment) {
|
|
42
|
+
const actual = await client.getChainId();
|
|
43
|
+
if (actual !== deployment.chainId) {
|
|
44
|
+
throw new Error(`wrong chain: client is on ${actual}, ${deployment.name} is ${deployment.chainId}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a revert into something a user can act on.
|
|
3
|
+
*
|
|
4
|
+
* A wallet that cannot estimate gas has no fee to show and no way to say why, so a reverting
|
|
5
|
+
* buy surfaces as "Network fee: Unavailable" or a bare selector. Neither tells anyone what to
|
|
6
|
+
* do next. Partners get the same mapping the front end uses, so a bot can explain the refusal
|
|
7
|
+
* in the chat instead of relaying hex.
|
|
8
|
+
*/
|
|
9
|
+
export declare const REVERT_REASONS: Record<string, string>;
|
|
10
|
+
export declare function revertSelector(err: unknown): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* A human explanation for a failed call, or null if it cannot be attributed.
|
|
13
|
+
*
|
|
14
|
+
* Returns null rather than guessing, so a caller can distinguish "the contract refused, and
|
|
15
|
+
* here is why" from "something else went wrong" -- a transport failure must not be reported as
|
|
16
|
+
* a rejected trade.
|
|
17
|
+
*/
|
|
18
|
+
export declare function explainRevert(err: unknown): string | null;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a revert into something a user can act on.
|
|
3
|
+
*
|
|
4
|
+
* A wallet that cannot estimate gas has no fee to show and no way to say why, so a reverting
|
|
5
|
+
* buy surfaces as "Network fee: Unavailable" or a bare selector. Neither tells anyone what to
|
|
6
|
+
* do next. Partners get the same mapping the front end uses, so a bot can explain the refusal
|
|
7
|
+
* in the chat instead of relaying hex.
|
|
8
|
+
*/
|
|
9
|
+
export const REVERT_REASONS = {
|
|
10
|
+
// --- who is asking ---
|
|
11
|
+
"0xcebd1b85": "You launched this token, so you cannot insure it. Covering your own launch and then " +
|
|
12
|
+
"collapsing it is the attack this refusal exists to stop. Switch to a different wallet.",
|
|
13
|
+
"0xc12b6c65": "This wallet is not allowed to buy cover.",
|
|
14
|
+
"0x7623fb52": "This policy belongs to another wallet.",
|
|
15
|
+
// --- the amount ---
|
|
16
|
+
"0xeddee5b2": "That amount is below the minimum trade size. Try a larger amount.",
|
|
17
|
+
"0x04213078": "That amount is above the maximum trade size. Try a smaller amount.",
|
|
18
|
+
"0x83ebf0b2": "You have reached the limit on how much cover one wallet may hold on this token. Let an " +
|
|
19
|
+
"existing policy expire, or cover a different token.",
|
|
20
|
+
"0x1bdfed9c": "This token has reached its cover limit. Let an existing policy expire, or cover a " +
|
|
21
|
+
"different token.",
|
|
22
|
+
"0x40b3c2e2": "This token has as much cover written against it as it is allowed. Wait for an existing " +
|
|
23
|
+
"policy to expire, or cover a different token.",
|
|
24
|
+
"0xbeb57309": "The pool has as much cover outstanding as it is allowed to carry. Try again once some " +
|
|
25
|
+
"existing policies expire.",
|
|
26
|
+
"0xce7b4a4d": "The pool does not hold enough reserves to cover this. Try a smaller amount.",
|
|
27
|
+
"0x1f2a2005": "Enter an amount above zero.",
|
|
28
|
+
"0x7c946ed7": "Enter an amount above zero.",
|
|
29
|
+
"0x5fc23da0": "This position would be too large a share of the token's supply to reach its own trigger, " +
|
|
30
|
+
"so cover on it could never pay out. Try a smaller amount.",
|
|
31
|
+
// --- the price ---
|
|
32
|
+
"0xb68adad1": "The price has not risen far enough above the launch price for this cover level. A payout " +
|
|
33
|
+
"must stay below the real loss, which needs a higher entry. Try a lower cover level.",
|
|
34
|
+
"0x11f66e3a": "The price moved too far while this was being prepared. Refresh and try again.",
|
|
35
|
+
"0x7dd37f70": "The price moved while this was being sent. Try again.",
|
|
36
|
+
"0xf2ab8ff0": "This token has no launch price recorded yet, so cover cannot be priced.",
|
|
37
|
+
"0xaf110036": "At this amount and cover level the payout works out to zero. Try a larger amount.",
|
|
38
|
+
// --- the token or venue ---
|
|
39
|
+
"0xc186028c": "No trading venue is registered for this token, so it cannot be bought here.",
|
|
40
|
+
"0x6586e129": "There is no route to trade this token right now.",
|
|
41
|
+
"0x8698bf37": "This token is not recognised by the protocol.",
|
|
42
|
+
"0x94d366b5": "That cover level is currently switched off. Pick another.",
|
|
43
|
+
"0x21a9b143": "That cover level does not exist.",
|
|
44
|
+
"0xc4e5ad09": "This token has no price history yet. Cover needs a short history before it can be priced.",
|
|
45
|
+
// --- state ---
|
|
46
|
+
"0x80cb55e2": "This policy is no longer active.",
|
|
47
|
+
"0x2f23cfcd": "The cover window for this policy has closed.",
|
|
48
|
+
"0xcbc95a6c": "This policy has not been triggered, so there is nothing to claim yet.",
|
|
49
|
+
"0x969bf728": "There is nothing to claim on this policy.",
|
|
50
|
+
"0x6b49c94a": "The insured position is no longer held.",
|
|
51
|
+
"0xf0b778da": "This policy is still active.",
|
|
52
|
+
"0x5a65d188": "Deposits are paused right now.",
|
|
53
|
+
};
|
|
54
|
+
export function revertSelector(err) {
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
const walk = (node, depth) => {
|
|
57
|
+
if (depth > 8 || node == null || typeof node !== "object" || seen.has(node))
|
|
58
|
+
return null;
|
|
59
|
+
seen.add(node);
|
|
60
|
+
const e = node;
|
|
61
|
+
// viem sometimes decodes it for us and puts the name here instead of raw bytes.
|
|
62
|
+
const raw = e.data;
|
|
63
|
+
if (typeof raw === "string" && /^0x[0-9a-fA-F]{8,}$/.test(raw))
|
|
64
|
+
return raw;
|
|
65
|
+
if (raw && typeof raw === "object") {
|
|
66
|
+
const inner = raw.data;
|
|
67
|
+
if (typeof inner === "string" && /^0x[0-9a-fA-F]{8,}$/.test(inner))
|
|
68
|
+
return inner;
|
|
69
|
+
}
|
|
70
|
+
for (const key of ["cause", "error", "info", "details"]) {
|
|
71
|
+
const found = walk(e[key], depth + 1);
|
|
72
|
+
if (found)
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
const fromGraph = typeof err === "object" ? walk(err, 0) : null;
|
|
78
|
+
if (fromGraph)
|
|
79
|
+
return fromGraph.slice(0, 10).toLowerCase();
|
|
80
|
+
// Only hex introduced as revert data -- never a loose address that happens to be nearby.
|
|
81
|
+
const text = typeof err === "string"
|
|
82
|
+
? err
|
|
83
|
+
: String(err?.message ?? err ?? "");
|
|
84
|
+
const keyed = text.match(/(?:data|reverted with|custom error)\D{0,12}(0x[0-9a-fA-F]{8,})/i);
|
|
85
|
+
return keyed ? keyed[1].slice(0, 10).toLowerCase() : null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A human explanation for a failed call, or null if it cannot be attributed.
|
|
89
|
+
*
|
|
90
|
+
* Returns null rather than guessing, so a caller can distinguish "the contract refused, and
|
|
91
|
+
* here is why" from "something else went wrong" -- a transport failure must not be reported as
|
|
92
|
+
* a rejected trade.
|
|
93
|
+
*/
|
|
94
|
+
export function explainRevert(err) {
|
|
95
|
+
const sel = revertSelector(err);
|
|
96
|
+
if (!sel)
|
|
97
|
+
return null;
|
|
98
|
+
return REVERT_REASONS[sel] ?? `This transaction would fail on chain (${sel}).`;
|
|
99
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a user is actually allowed to buy.
|
|
3
|
+
*
|
|
4
|
+
* Three separate limits decide it and they are denominated in three different things: the
|
|
5
|
+
* registry's minimum and maximum are notional wei, remaining capacity is a payout, and the
|
|
6
|
+
* share cap counts tokens. A user typing one number into one box cannot reconcile those, so a
|
|
7
|
+
* partner that does not do this arithmetic can only relay refusals after the fact -- which is
|
|
8
|
+
* what "This transaction would fail on chain (0xeddee5b2)" looked like before the front end
|
|
9
|
+
* grew the same helpers.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BPS = 10000n;
|
|
12
|
+
export interface Tier {
|
|
13
|
+
id: number;
|
|
14
|
+
name: string;
|
|
15
|
+
premiumBps: bigint;
|
|
16
|
+
payoutBps: bigint;
|
|
17
|
+
/** Entry price must be at least this many bps of the floor for the tier to be offered. */
|
|
18
|
+
minEntryBps: bigint;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Robinhood Chain's rate card, and nobody else's.
|
|
22
|
+
*
|
|
23
|
+
* Named for the chain it was measured on, because that is the whole of its validity. The
|
|
24
|
+
* premiums come from a calibration run over 4,679 pools and 301,612 buys on Robinhood Chain;
|
|
25
|
+
* `Chains.card()` reverts `NoRateCard` for every other chain rather than reuse them, and this
|
|
26
|
+
* SDK holds the same line. A premium is a claim about how often a token collapses, and that
|
|
27
|
+
* is a property of a chain's flow, not of the product.
|
|
28
|
+
*
|
|
29
|
+
* Use it to render a quote before you have a connection. Once you have one, read the card off
|
|
30
|
+
* the registry with `readRateCard` -- a deployment may have been reconfigured, and on any
|
|
31
|
+
* chain but Robinhood these numbers were never true to begin with.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ROBINHOOD_RATE_CARD: readonly Tier[];
|
|
34
|
+
export declare function tierById(card: readonly Tier[], id: number): Tier;
|
|
35
|
+
/** The slice of a viem public client the rate-card read needs. */
|
|
36
|
+
export interface TierReader {
|
|
37
|
+
readContract(args: {
|
|
38
|
+
address: `0x${string}`;
|
|
39
|
+
abi: readonly unknown[];
|
|
40
|
+
functionName: string;
|
|
41
|
+
args?: readonly unknown[];
|
|
42
|
+
}): Promise<unknown>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read the live rate card off a deployment's registry.
|
|
46
|
+
*
|
|
47
|
+
* The premium, the payout and the minimum entry are all product terms the contract enforces,
|
|
48
|
+
* so all three are read. Only the display name is local.
|
|
49
|
+
*/
|
|
50
|
+
export declare function readRateCard(reader: TierReader, registry: `0x${string}`, abi: readonly unknown[], ids?: readonly number[]): Promise<Tier[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Throw if a local card disagrees with the chain's.
|
|
53
|
+
*
|
|
54
|
+
* For callers who quote from `ROBINHOOD_RATE_CARD` before connecting: this is what turns a
|
|
55
|
+
* silent mispricing into a loud failure the first time it happens. Quoting one chain's
|
|
56
|
+
* premiums against another chain's risk is the failure mode worth being noisy about, because
|
|
57
|
+
* nothing downstream of it looks wrong -- the trade succeeds, at the wrong price.
|
|
58
|
+
*/
|
|
59
|
+
export declare function assertRateCard(local: readonly Tier[], onChain: readonly Tier[]): void;
|
|
60
|
+
/** Split a total payment into the trade and the premium, exactly as the router does. */
|
|
61
|
+
export declare function splitValue(total: bigint, premiumBps: bigint): {
|
|
62
|
+
notional: bigint;
|
|
63
|
+
premium: bigint;
|
|
64
|
+
};
|
|
65
|
+
/** A notional, grossed up to the total a user pays -- trade plus premium. */
|
|
66
|
+
export declare function totalForNotional(notional: bigint, tier: Tier): bigint;
|
|
67
|
+
/**
|
|
68
|
+
* Largest total spendable before the payout exceeds what is left on a token.
|
|
69
|
+
*
|
|
70
|
+
* Remaining capacity is a *payout* figure, and a user types a *trade size*. They differ by the
|
|
71
|
+
* tier -- 2.2x at Degen Max, 6.9x at Basic -- so relaying the capacity number itself tells a
|
|
72
|
+
* user to spend far less than they could.
|
|
73
|
+
*/
|
|
74
|
+
export declare function maxTotalForCapacity(capacity: bigint, tier: Tier): bigint;
|
|
75
|
+
/** Pool state needed to price a trade, as the venue reports it. */
|
|
76
|
+
export interface PoolQuote {
|
|
77
|
+
quoteReserve: bigint;
|
|
78
|
+
tokenReserve: bigint;
|
|
79
|
+
feeBps: bigint;
|
|
80
|
+
launchPrice: bigint;
|
|
81
|
+
}
|
|
82
|
+
/** Tokens a payment buys, from the pool's own constant-product maths. */
|
|
83
|
+
export declare function quoteOut(q: PoolQuote, notional: bigint): bigint;
|
|
84
|
+
/**
|
|
85
|
+
* The inverse of `quoteOut`: the largest payment that buys no more than `tokens`.
|
|
86
|
+
*
|
|
87
|
+
* The position-share cap is a share of supply, so it is expressed in tokens while the user
|
|
88
|
+
* pays in the quote asset. Rounds down, and settles the last wei against `quoteOut` rather
|
|
89
|
+
* than trusting the algebra -- integer division lands either side of the true answer depending
|
|
90
|
+
* on the residue, and "the maximum" being one wei too large is the whole bug this prevents.
|
|
91
|
+
*/
|
|
92
|
+
export declare function notionalForTokens(q: PoolQuote, tokens: bigint): bigint;
|
|
93
|
+
export interface RiskLimits {
|
|
94
|
+
minTradeWei: bigint;
|
|
95
|
+
perTradeMaxWei: bigint;
|
|
96
|
+
/** Share of supply one wallet may insure on one token. */
|
|
97
|
+
maxPositionShareBps: bigint;
|
|
98
|
+
/** Share of supply insurable on one token across every wallet. */
|
|
99
|
+
maxTokenInsuredShareBps: bigint;
|
|
100
|
+
/**
|
|
101
|
+
* Largest notional the share caps still allow, converted from tokens at the current price.
|
|
102
|
+
*
|
|
103
|
+
* Leave undefined when it cannot be worked out. The bound is then omitted rather than
|
|
104
|
+
* guessed: the contract enforces it either way, and a fabricated limit is worse than a
|
|
105
|
+
* missing one.
|
|
106
|
+
*/
|
|
107
|
+
maxShareNotionalWei?: bigint;
|
|
108
|
+
}
|
|
109
|
+
export interface AmountRange {
|
|
110
|
+
min: bigint;
|
|
111
|
+
max: bigint;
|
|
112
|
+
/** False when the limits leave no buyable amount at all. */
|
|
113
|
+
usable: boolean;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The band of totals a buy will actually be accepted at.
|
|
117
|
+
*
|
|
118
|
+
* A zero limit means "not known", never "capped at zero" -- treating an unloaded bound as a
|
|
119
|
+
* hard zero collapses the band and refuses everything.
|
|
120
|
+
*
|
|
121
|
+
* The share bound is shaved by 1%. It counts tokens while the user pays in the quote asset,
|
|
122
|
+
* and the rate between them moves on every trade, so a price fall between quoting a maximum
|
|
123
|
+
* and signing it means the same payment buys more. Sitting exactly on the limit is how a
|
|
124
|
+
* stated maximum still gets refused.
|
|
125
|
+
*/
|
|
126
|
+
export declare function amountRange(capacity: bigint, tier: Tier, limits: RiskLimits): AmountRange;
|
|
127
|
+
/**
|
|
128
|
+
* Tokens a wallet may still insure on a token, before either share cap binds.
|
|
129
|
+
*
|
|
130
|
+
* Both caps are cumulative and both are a share of supply: one per wallet, one across all
|
|
131
|
+
* wallets. Whichever has less room left decides.
|
|
132
|
+
*/
|
|
133
|
+
export declare function shareHeadroom(supply: bigint, walletTokensInsured: bigint, tokenTokensInsured: bigint, limits: Pick<RiskLimits, "maxPositionShareBps" | "maxTokenInsuredShareBps">): bigint;
|
|
134
|
+
/** Decode the `getParams()` tuple into the limits this module works in. */
|
|
135
|
+
export declare function riskLimitsFromParams(p: readonly (bigint | number)[]): RiskLimits;
|