ifr-sdk 0.0.0-stage → 0.4.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Inferno Protocol contributors
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 CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # IFR SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Repository SDK for exact 9-decimal IFR access checks and signer-neutral Benefits checkout
4
+ sessions. The package currently supports Ethereum Mainnet only.
5
+
6
+ ## Availability
7
+
8
+ The SDK is not yet published to the npm registry. Publication is prepared: the first version is a
9
+ one-time manual bootstrap by the project npm account; every later version is published by
10
+ `.github/workflows/sdk-publish.yml`, dispatched manually on protected `main`, which checks the release
11
+ gate, waits for owner approval, tests the package and publishes it with npm provenance through npm
12
+ Trusted Publishing. Until then, build and pack the versioned artifact from the repository:
13
+
14
+ ```bash
15
+ cd apps/sdk
16
+ npm ci
17
+ npm run build
18
+ npm pack --ignore-scripts
19
+ ```
20
+
21
+ Add the resulting `.tgz` path to the consuming application's `package.json` and committed
22
+ lockfile, then use `npm ci`.
23
+
24
+ ## Contract Addresses
25
+
26
+ `MAINNET_ADDRESSES` lists deployed contracts. `commitmentVault` is CommitmentVault V1: its
27
+ price-conditioned tranches can never unlock, so applications must not create new locks there;
28
+ existing time tranches stay unlockable through V1. `commitmentVaultV2` is CommitmentVault V2
29
+ (TIME_ONLY); it accepts new time locks once Governance proposal #17 (its fee exemption) has executed.
30
+ `lendingVault` is the retired LendingVault V1 with borrowing permanently disabled.
31
+
32
+ The tarball exports CommonJS with tested ESM named-import interoperability and supports Node.js
33
+ 20 and 22. CI verifies the exact package contents, installs the locked tarball with `npm ci`, and
34
+ runs fresh CommonJS, ESM-import and TypeScript consumers.
35
+
36
+ The canonical REST API is:
37
+
38
+ ```text
39
+ https://copilot-api.ifrunit.tech/api/ifr/check
40
+ ```
41
+
42
+ ## Benefit Tiers
43
+
44
+ `DEFAULT_TIERS` is the project's default preset and matches the Benefits network: IFR **locked in
45
+ IFRLock** only, Bronze 1,000 / Silver 2,500 / Gold 5,000 / Platinum 10,000.
46
+
47
+ ```js
48
+ const { IFRClient, DEFAULT_TIERS, getBenefitTierFromRaw } = require("ifr-sdk");
49
+ const client = new IFRClient();
50
+ const tier = await client.getBenefitTier(wallet); // default preset
51
+ const own = await client.getBenefitTier(wallet, [ // your own model
52
+ { key: "fan", name: "Fan", minLocked: "100" },
53
+ { key: "vip", name: "VIP", minLocked: "2000" },
54
+ ]);
55
+ ```
56
+
57
+ - The tiers are a **default preset and label set, not a rule**. In the Benefits network every partner
58
+ sets its own thresholds, minimum held IFR, lock source (IFRLock, CommitmentVault time locks or
59
+ either), discount and daily/monthly limits per benefit.
60
+ - Any project may verify IFR permissionlessly with its own rule, for example a hold-based check with
61
+ its own discount, following the open verification profile `ifr-benefits-verify/1`.
62
+ - Discounts are independent of partner rewards. PartnerVault rewards follow Lane 4 model B and are
63
+ available only to approved pilot partners; they are currently disabled.
64
+ - `getBenefitTier` reads IFRLock only and fails closed: a failed read throws instead of reporting 0.
65
+ - `TIER_THRESHOLDS`, `TIER_NAMES`, `getTier`, `getTierFromRaw` and the `tier` fields of `checkAccess` are
66
+ the deprecated legacy hold+lock access tiers (balance + locked, 500 / 2,000 / 10,000). They remain
67
+ for compatibility and are removed in 1.0.
68
+
69
+ ## Read Errors
70
+
71
+ The SDK never turns a failed on-chain read into `0` or `false`. If a read fails, `checkAccess`, `getTier`,
72
+ `getBenefitTier`, `getLockedBalance` and `isBuilder` reject with `IFRReadError` (its `read` field names the
73
+ failed call, `cause` holds the original error). Treat it as "could not verify", not as "nothing locked" or
74
+ "not a builder".
75
+
76
+ ## Benefits Checkout
77
+
78
+ `IFRBenefitsClient.createCheckout()` requests a one-time `sessions:create` challenge bound to
79
+ the business and benefit rule (the request carries no wallet; the backend recovers the seller from
80
+ the signature), then creates the session.
81
+ `IFRBenefitsClient.getCheckoutStatus(sessionId)` polls `GET /api/sessions/:id` and fail-closed
82
+ validates the public status response (`PENDING`, `APPROVED`, `REJECTED`, `REDEEMED`, `EXPIRED`).
83
+ `IFRBenefitsClient.redeemCheckout({ sessionId, walletAddress, signMessage })` requests a fresh
84
+ one-time `sessions:redeem` challenge bound to the session ID and posts the signed redemption to
85
+ `POST /api/sessions/:id/redeem`; it resolves only after the API confirms `REDEEMED`.
86
+
87
+ Both signed flows validate action, business, scope, domain, chain, timestamp, nonce and the exact
88
+ challenge message before any signature is requested.
89
+
90
+ ### Compatibility: wallet-free seller challenge (0.4.0)
91
+
92
+ The Benefits backend no longer echoes `walletAddress` in `GET /api/seller/auth-message` (owner
93
+ decision B: no wallet in the challenge URL, response or storage; a legacy `walletAddress` query
94
+ parameter is ignored). Earlier `ifr-sdk` builds up to 0.3.0 (never published to npm; only repository
95
+ tarballs) require `challenge.walletAddress` in that response and reject every challenge from the new backend ("mismatched seller authorization
96
+ challenge"), so `createCheckout()` and `redeemCheckout()` break. Release order: publish and roll out
97
+ `ifr-sdk` 0.4.0 (wallet-free) **before** the Benefits backend release that ships the
98
+ wallet-free challenge, and tell integrators to upgrade first. Publishing is a separate release gate. The caller supplies a wallet-native `signMessage`
99
+ callback; the SDK never accepts or stores private keys, seed phrases or persistent seller secrets.
100
+
101
+ ## Verification
102
+
103
+ ```bash
104
+ cd apps/sdk
105
+ npm ci
106
+ npm test
107
+ npm run test:package
108
+ npm audit --audit-level=moderate
109
+ npm pack --dry-run
110
+ ```
111
+
112
+ Publication follows the release checklist in `docs/runbooks/IFR_SDK_NPM_RELEASE.md`.
@@ -0,0 +1,71 @@
1
+ export declare const DEFAULT_BENEFITS_API = "https://shop.ifrunit.tech";
2
+ export type SellerAuthorizationAction = "sessions:create" | "sessions:redeem";
3
+ export interface SellerAuthorizationChallenge {
4
+ action: SellerAuthorizationAction;
5
+ businessId: string;
6
+ scope: string;
7
+ domain: string;
8
+ chainId: number;
9
+ timestamp: string;
10
+ issuedAt: string;
11
+ expiresAt: string;
12
+ message: string;
13
+ nonce: string;
14
+ }
15
+ export interface BenefitsCheckoutSession {
16
+ sessionId: string;
17
+ expiresAt: string;
18
+ qrUrl: string;
19
+ customerUrl: string;
20
+ benefitRuleId: string | null;
21
+ label: string | null;
22
+ category: string | null;
23
+ productName: string | null;
24
+ discountPercent: number;
25
+ requiredLockIFR: number;
26
+ dailyRedemptionLimit: number;
27
+ monthlyRedemptionLimit: number;
28
+ tierLabel: string | null;
29
+ }
30
+ export interface CreateBenefitsCheckoutParams {
31
+ businessId: string;
32
+ benefitRuleId?: string;
33
+ walletAddress: string;
34
+ signMessage: (message: string) => Promise<string>;
35
+ }
36
+ export interface RedeemBenefitsCheckoutParams {
37
+ sessionId: string;
38
+ walletAddress: string;
39
+ signMessage: (message: string) => Promise<string>;
40
+ }
41
+ export type BenefitsCheckoutStatusValue = "PENDING" | "APPROVED" | "REJECTED" | "REDEEMED" | "EXPIRED";
42
+ export interface BenefitsCheckoutStatus {
43
+ status: BenefitsCheckoutStatusValue;
44
+ reason: string | null;
45
+ redeemedAt: string | null;
46
+ expiresAt: string;
47
+ attestAttempts: number;
48
+ businessId: string;
49
+ benefitRuleId: string | null;
50
+ presentation: "CUSTOMER_PASS" | "SELLER_QR";
51
+ }
52
+ export interface BenefitsCheckoutRedemption {
53
+ status: "REDEEMED";
54
+ }
55
+ export interface IFRBenefitsClientConfig {
56
+ baseUrl?: string;
57
+ fetch?: typeof fetch;
58
+ /** Expected chain ID of the Benefits deployment; seller challenges for another chain are refused. */
59
+ chainId?: number;
60
+ }
61
+ export declare class IFRBenefitsClient {
62
+ private readonly baseUrl;
63
+ private readonly fetchImpl;
64
+ private readonly chainId?;
65
+ constructor(config?: IFRBenefitsClientConfig);
66
+ private requestSellerChallenge;
67
+ private signSellerChallenge;
68
+ createCheckout(params: CreateBenefitsCheckoutParams): Promise<BenefitsCheckoutSession>;
69
+ getCheckoutStatus(sessionId: string): Promise<BenefitsCheckoutStatus>;
70
+ redeemCheckout(params: RedeemBenefitsCheckoutParams): Promise<BenefitsCheckoutRedemption>;
71
+ }
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IFRBenefitsClient = exports.DEFAULT_BENEFITS_API = void 0;
4
+ exports.DEFAULT_BENEFITS_API = "https://shop.ifrunit.tech";
5
+ const SELLER_AUTH_TTL_MS = 10 * 60 * 1000;
6
+ const MAX_FUTURE_SKEW_MS = 2 * 60 * 1000;
7
+ const BENEFITS_CHECKOUT_STATUSES = [
8
+ "PENDING",
9
+ "APPROVED",
10
+ "REJECTED",
11
+ "REDEEMED",
12
+ "EXPIRED",
13
+ ];
14
+ function safeAuthorizationField(value, label) {
15
+ if (!value || value.length > 200 || value !== value.trim() || /[\r\n]/.test(value)) {
16
+ throw new Error(`Invalid ${label}`);
17
+ }
18
+ return value;
19
+ }
20
+ function safeSessionPathSegment(value) {
21
+ if (!value ||
22
+ value.length > 200 ||
23
+ value !== value.trim() ||
24
+ /[\u0000-\u001f\u007f]/.test(value) ||
25
+ /[/?#\\]/.test(value) ||
26
+ value.includes("..")) {
27
+ throw new Error("Invalid checkout session ID");
28
+ }
29
+ return encodeURIComponent(value);
30
+ }
31
+ function normalizeBaseUrl(value) {
32
+ const url = new URL(value);
33
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
34
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
35
+ throw new Error("Benefits API must use HTTPS except on loopback test hosts");
36
+ }
37
+ if (url.username || url.password || (url.pathname !== "/" && url.pathname !== "") || url.search || url.hash) {
38
+ throw new Error("Benefits API must be an origin without credentials, path, query, or fragment");
39
+ }
40
+ url.pathname = "/";
41
+ return url;
42
+ }
43
+ function isAddress(value) {
44
+ return /^0x[0-9a-fA-F]{40}$/.test(value);
45
+ }
46
+ function isCanonicalIsoDate(value) {
47
+ if (typeof value !== "string")
48
+ return false;
49
+ const timestamp = Date.parse(value);
50
+ return !Number.isNaN(timestamp) && new Date(timestamp).toISOString() === value;
51
+ }
52
+ async function readJson(response) {
53
+ if (!response.ok)
54
+ throw new Error(`IFR Benefits API request failed (${response.status})`);
55
+ return response.json();
56
+ }
57
+ function buildSellerAuthorizationMessage(domain, chainId, action, businessId, timestamp, scope, nonce) {
58
+ return [
59
+ "IFR Benefits Network - Seller Authorization",
60
+ `Domain: ${domain}`,
61
+ `Chain ID: ${chainId}`,
62
+ `Action: ${action}`,
63
+ `Business: ${businessId}`,
64
+ `Scope: ${scope}`,
65
+ `Nonce: ${nonce}`,
66
+ `Timestamp: ${timestamp}`,
67
+ `Expires: ${new Date(Number(timestamp) + SELLER_AUTH_TTL_MS).toISOString()}`,
68
+ `Only sign this message inside ${domain}.`,
69
+ ].join("\n");
70
+ }
71
+ class IFRBenefitsClient {
72
+ constructor(config = {}) {
73
+ this.baseUrl = normalizeBaseUrl(config.baseUrl || exports.DEFAULT_BENEFITS_API);
74
+ this.fetchImpl = config.fetch || fetch;
75
+ if (config.chainId !== undefined && (!Number.isSafeInteger(config.chainId) || config.chainId <= 0)) {
76
+ throw new Error("Invalid Benefits chain ID");
77
+ }
78
+ this.chainId = config.chainId;
79
+ }
80
+ async requestSellerChallenge(action, businessId, scope) {
81
+ // The challenge request is wallet-free (no address in the URL): the signer is bound only by the
82
+ // signature over the returned message, which the backend recovers.
83
+ const challengeUrl = new URL("/api/seller/auth-message", this.baseUrl);
84
+ challengeUrl.search = new URLSearchParams({
85
+ action,
86
+ businessId,
87
+ scope,
88
+ }).toString();
89
+ const challenge = await readJson(await this.fetchImpl(challengeUrl.toString(), { method: "GET" }));
90
+ const timestampMs = Number(challenge.timestamp);
91
+ const issuedAtMs = Date.parse(challenge.issuedAt);
92
+ const expiresAtMs = Date.parse(challenge.expiresAt);
93
+ const now = Date.now();
94
+ if (challenge.action !== action ||
95
+ challenge.businessId !== businessId ||
96
+ challenge.scope !== scope ||
97
+ // The signed message must name the exact API origin host this client talks to.
98
+ challenge.domain !== this.baseUrl.host ||
99
+ !Number.isSafeInteger(challenge.chainId) ||
100
+ challenge.chainId <= 0 ||
101
+ (this.chainId !== undefined && challenge.chainId !== this.chainId) ||
102
+ !/^\d{10,16}$/.test(challenge.timestamp || "") ||
103
+ !Number.isSafeInteger(timestampMs) ||
104
+ issuedAtMs !== timestampMs ||
105
+ expiresAtMs !== timestampMs + SELLER_AUTH_TTL_MS ||
106
+ timestampMs < now - SELLER_AUTH_TTL_MS ||
107
+ timestampMs > now + MAX_FUTURE_SKEW_MS ||
108
+ expiresAtMs <= now ||
109
+ !/^[0-9a-f]{64}$/.test(challenge.nonce || "") ||
110
+ challenge.message !== buildSellerAuthorizationMessage(challenge.domain, challenge.chainId, action, businessId, challenge.timestamp, scope, challenge.nonce)) {
111
+ throw new Error("IFR Benefits API returned a mismatched seller authorization challenge");
112
+ }
113
+ return challenge;
114
+ }
115
+ async signSellerChallenge(challenge, signMessage) {
116
+ const signature = await signMessage(challenge.message);
117
+ if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) {
118
+ throw new Error("Seller wallet returned an invalid signature");
119
+ }
120
+ return signature;
121
+ }
122
+ async createCheckout(params) {
123
+ const businessId = safeAuthorizationField(params.businessId, "business ID");
124
+ const scope = safeAuthorizationField(params.benefitRuleId || "default", "benefit rule ID");
125
+ if (!isAddress(params.walletAddress))
126
+ throw new Error("Invalid seller wallet address");
127
+ if (typeof params.signMessage !== "function")
128
+ throw new Error("Seller wallet signer is required");
129
+ const challenge = await this.requestSellerChallenge("sessions:create", businessId, scope);
130
+ const signature = await this.signSellerChallenge(challenge, params.signMessage);
131
+ const sessionUrl = new URL("/api/sessions", this.baseUrl);
132
+ const session = await readJson(await this.fetchImpl(sessionUrl.toString(), {
133
+ method: "POST",
134
+ headers: {
135
+ "content-type": "application/json",
136
+ "x-ifr-wallet": params.walletAddress,
137
+ "x-ifr-signature": signature,
138
+ "x-ifr-timestamp": challenge.timestamp,
139
+ "x-ifr-nonce": challenge.nonce,
140
+ },
141
+ body: JSON.stringify({
142
+ businessId,
143
+ ...(params.benefitRuleId ? { benefitRuleId: params.benefitRuleId } : {}),
144
+ }),
145
+ }));
146
+ if (!session.sessionId || !session.qrUrl) {
147
+ throw new Error("IFR Benefits API returned an invalid checkout session");
148
+ }
149
+ const customerUrl = new URL(session.qrUrl, this.baseUrl);
150
+ if (customerUrl.origin !== this.baseUrl.origin ||
151
+ !/^\/r\/[^/]+$/.test(customerUrl.pathname) ||
152
+ customerUrl.search ||
153
+ customerUrl.hash) {
154
+ throw new Error("IFR Benefits API returned an invalid customer URL");
155
+ }
156
+ return {
157
+ ...session,
158
+ customerUrl: customerUrl.toString(),
159
+ };
160
+ }
161
+ async getCheckoutStatus(sessionId) {
162
+ const segment = safeSessionPathSegment(sessionId);
163
+ const statusUrl = new URL(`/api/sessions/${segment}`, this.baseUrl);
164
+ const session = await readJson(await this.fetchImpl(statusUrl.toString(), { method: "GET", cache: "no-store" }));
165
+ if (!session || typeof session !== "object") {
166
+ throw new Error("IFR Benefits API returned an invalid checkout status");
167
+ }
168
+ if (!BENEFITS_CHECKOUT_STATUSES.includes(session.status) ||
169
+ !(session.reason === null || typeof session.reason === "string") ||
170
+ !(session.redeemedAt === null || isCanonicalIsoDate(session.redeemedAt)) ||
171
+ !isCanonicalIsoDate(session.expiresAt) ||
172
+ !Number.isSafeInteger(session.attestAttempts) ||
173
+ session.attestAttempts < 0 ||
174
+ typeof session.businessId !== "string" ||
175
+ !session.businessId ||
176
+ !(session.benefitRuleId === null || typeof session.benefitRuleId === "string") ||
177
+ (session.presentation !== "CUSTOMER_PASS" && session.presentation !== "SELLER_QR")) {
178
+ throw new Error("IFR Benefits API returned an invalid checkout status");
179
+ }
180
+ return {
181
+ status: session.status,
182
+ reason: session.reason,
183
+ redeemedAt: session.redeemedAt,
184
+ expiresAt: session.expiresAt,
185
+ attestAttempts: session.attestAttempts,
186
+ businessId: session.businessId,
187
+ benefitRuleId: session.benefitRuleId,
188
+ presentation: session.presentation,
189
+ };
190
+ }
191
+ async redeemCheckout(params) {
192
+ const segment = safeSessionPathSegment(params.sessionId);
193
+ if (!isAddress(params.walletAddress))
194
+ throw new Error("Invalid seller wallet address");
195
+ if (typeof params.signMessage !== "function")
196
+ throw new Error("Seller wallet signer is required");
197
+ const challenge = await this.requestSellerChallenge("sessions:redeem", params.sessionId, params.sessionId);
198
+ const signature = await this.signSellerChallenge(challenge, params.signMessage);
199
+ const redeemUrl = new URL(`/api/sessions/${segment}/redeem`, this.baseUrl);
200
+ const result = await readJson(await this.fetchImpl(redeemUrl.toString(), {
201
+ method: "POST",
202
+ headers: {
203
+ "x-ifr-wallet": params.walletAddress,
204
+ "x-ifr-signature": signature,
205
+ "x-ifr-timestamp": challenge.timestamp,
206
+ "x-ifr-nonce": challenge.nonce,
207
+ },
208
+ }));
209
+ if (!result || result.status !== "REDEEMED") {
210
+ throw new Error("IFR Benefits API returned an invalid checkout redemption");
211
+ }
212
+ return { status: "REDEEMED" };
213
+ }
214
+ }
215
+ exports.IFRBenefitsClient = IFRBenefitsClient;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * IFR SDK — Web3 Access Control for Builders
3
+ *
4
+ * Repository package: build and pack apps/sdk, then consume the locked tarball
5
+ *
6
+ * import { IFRClient } from "ifr-sdk"
7
+ * const ifr = new IFRClient({ network: "mainnet" })
8
+ * const access = await ifr.checkAccess({ wallet: "0x...", required: 1000 })
9
+ * if (access.hasAccess) enableAccess()
10
+ */
11
+ import { type BigNumberish } from "ethers";
12
+ export * from "./benefits";
13
+ export declare const MAINNET_ADDRESSES: {
14
+ readonly ifrToken: "0x77e99917Eca8539c62F509ED1193ac36580A6e7B";
15
+ readonly ifrLock: "0x769928aBDfc949D0718d8766a1C2d7dBb63954Eb";
16
+ /** CommitmentVault V1: price tranches never unlock (CV-01); existing time tranches unlockable. Do not create new locks here. */
17
+ readonly commitmentVault: "0x0719d9eb28dF7f5e63F91fAc4Bbb2d579C4F73d3";
18
+ /** CommitmentVault V2: TIME_ONLY; new locks after Governance proposal #17 (fee exemption) executes. */
19
+ readonly commitmentVaultV2: "0x8efae0C85ad6d44C731cAEDA1cBC275904Fc7c8F";
20
+ /** LendingVault V1: retired, borrowing permanently disabled. */
21
+ readonly lendingVault: "0x974305Ab0EC905172e697271C3d7d385194EB9DF";
22
+ readonly builderRegistry: "0xdfe6636DA47F8949330697e1dC5391267CEf0EE3";
23
+ readonly governance: "0xc43d48E7FDA576C5022d0670B652A622E8caD041";
24
+ };
25
+ export declare const IFR_API = "https://copilot-api.ifrunit.tech";
26
+ /** @deprecated Use IFR_API. */
27
+ export declare const RAILWAY_API = "https://copilot-api.ifrunit.tech";
28
+ export declare const IFR_DECIMALS = 9;
29
+ export interface IFRClientConfig {
30
+ network?: "mainnet" | "sepolia";
31
+ rpcUrl?: string;
32
+ apiUrl?: string;
33
+ }
34
+ export interface AccessCheckParams {
35
+ wallet: string;
36
+ required: string | number;
37
+ checkLocked?: boolean;
38
+ }
39
+ export interface AccessResult {
40
+ hasAccess: boolean;
41
+ balance: number;
42
+ locked: number;
43
+ total: number;
44
+ required: number;
45
+ balanceRaw: string;
46
+ lockedRaw: string;
47
+ totalRaw: string;
48
+ requiredRaw: string;
49
+ tier: number;
50
+ tierName: string;
51
+ }
52
+ export interface TierResult {
53
+ tier: number;
54
+ tierName: string;
55
+ balance: number;
56
+ locked: number;
57
+ balanceRaw: string;
58
+ lockedRaw: string;
59
+ totalRaw: string;
60
+ }
61
+ export interface BenefitTier {
62
+ /** Stable key, e.g. "bronze". */
63
+ key: string;
64
+ /** Display name, e.g. "Bronze". */
65
+ name: string;
66
+ /** Minimum IFR locked in IFRLock, decimal string with at most 9 decimals. */
67
+ minLocked: string;
68
+ }
69
+ export declare const DEFAULT_TIERS: readonly BenefitTier[];
70
+ export interface BenefitTierResult {
71
+ /** 0 = below the first tier, 1..n = index of the highest tier reached (1-based). */
72
+ level: number;
73
+ /** Tier key, or null below the first tier. */
74
+ key: string | null;
75
+ /** Tier name, or "None" below the first tier. */
76
+ name: string;
77
+ lockedRaw: string;
78
+ }
79
+ /** Validates a tier list: non-empty, unique keys, strictly ascending exact 9-decimal thresholds. */
80
+ export declare function validateTiers(tiers: readonly BenefitTier[]): bigint[];
81
+ /** Highest tier reached by an exact locked amount (9-decimal base units); no rounding. */
82
+ export declare function getBenefitTierFromRaw(lockedRaw: BigNumberish, tiers?: readonly BenefitTier[]): BenefitTierResult;
83
+ /**
84
+ * Thrown when an on-chain read fails. The SDK never turns a failed read into 0 or false,
85
+ * so a caller cannot mistake an RPC outage for "no IFR locked" or "not a builder".
86
+ */
87
+ export declare class IFRReadError extends Error {
88
+ readonly read: string;
89
+ constructor(read: string, cause: unknown);
90
+ }
91
+ /**
92
+ * @deprecated Legacy hold+lock access tiers (wallet balance + locked, 500 / 2,000 / 10,000).
93
+ * Not the project's benefit preset. Use `DEFAULT_TIERS` with `getBenefitTierFromRaw`. Removed in 1.0.
94
+ */
95
+ export declare const TIER_THRESHOLDS: {
96
+ readonly TIER1: 500;
97
+ readonly TIER2: 2000;
98
+ readonly TIER3: 10000;
99
+ };
100
+ /** @deprecated Legacy access tier names; use `DEFAULT_TIERS`. Removed in 1.0. */
101
+ export declare const TIER_NAMES: readonly ["None", "Basic", "Premium", "Pro"];
102
+ /** @deprecated Legacy hold+lock access tier; use `getBenefitTierFromRaw`. Removed in 1.0. */
103
+ export declare function getTierFromAmount(amount: number): number;
104
+ /** @deprecated Legacy access tier name; use `getBenefitTierFromRaw(...).name`. Removed in 1.0. */
105
+ export declare function getTierName(tier: number): string;
106
+ export declare function parseIFRAmount(value: string | number): bigint;
107
+ /** @deprecated Legacy hold+lock access tier; use `getBenefitTierFromRaw`. Removed in 1.0. */
108
+ export declare function getTierFromRaw(amount: BigNumberish): number;
109
+ export declare function evaluateAccessRaw(balanceRaw: BigNumberish, lockedRaw: BigNumberish, requiredRaw: BigNumberish): {
110
+ hasAccess: boolean;
111
+ total: bigint;
112
+ tier: number;
113
+ tierName: string;
114
+ };
115
+ export declare class IFRClient {
116
+ private provider;
117
+ private token;
118
+ private lockContract;
119
+ private registry;
120
+ private apiUrl;
121
+ readonly TIER1: 500;
122
+ readonly TIER2: 2000;
123
+ readonly TIER3: 10000;
124
+ constructor(config?: IFRClientConfig);
125
+ /** Check if wallet has sufficient IFR (balance + locked) */
126
+ checkAccess(params: AccessCheckParams): Promise<AccessResult>;
127
+ /**
128
+ * Benefit tier from IFR locked in IFRLock only (default preset Bronze/Silver/Gold/Platinum, or your
129
+ * own `tiers`). Fails closed: a failed IFRLock read throws instead of reporting 0.
130
+ */
131
+ getBenefitTier(wallet: string, tiers?: readonly BenefitTier[]): Promise<BenefitTierResult>;
132
+ /**
133
+ * @deprecated Legacy hold+lock access tier (0=none, 1=basic, 2=premium, 3=pro; balance + locked,
134
+ * 500 / 2,000 / 10,000). Use `getBenefitTier`. Removed in 1.0.
135
+ */
136
+ getTier(wallet: string): Promise<TierResult>;
137
+ /** Get IFR wallet balance */
138
+ getBalance(wallet: string): Promise<number>;
139
+ /** Get locked IFR balance */
140
+ getLockedBalance(wallet: string): Promise<number>;
141
+ /** Check if address is a registered builder */
142
+ isBuilder(address: string): Promise<boolean>;
143
+ /** Get total IFR supply */
144
+ getTotalSupply(): Promise<number>;
145
+ /** REST API check — no ethers dependency needed on caller side */
146
+ static apiCheck(params: {
147
+ wallet: string;
148
+ required?: string | number;
149
+ apiUrl?: string;
150
+ }): Promise<{
151
+ hasAccess: boolean;
152
+ balance: string;
153
+ tier: number;
154
+ tierName: string;
155
+ }>;
156
+ }
157
+ export default IFRClient;
package/dist/index.js ADDED
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ /**
3
+ * IFR SDK — Web3 Access Control for Builders
4
+ *
5
+ * Repository package: build and pack apps/sdk, then consume the locked tarball
6
+ *
7
+ * import { IFRClient } from "ifr-sdk"
8
+ * const ifr = new IFRClient({ network: "mainnet" })
9
+ * const access = await ifr.checkAccess({ wallet: "0x...", required: 1000 })
10
+ * if (access.hasAccess) enableAccess()
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
24
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.IFRClient = exports.TIER_NAMES = exports.TIER_THRESHOLDS = exports.IFRReadError = exports.DEFAULT_TIERS = exports.IFR_DECIMALS = exports.RAILWAY_API = exports.IFR_API = exports.MAINNET_ADDRESSES = void 0;
28
+ exports.validateTiers = validateTiers;
29
+ exports.getBenefitTierFromRaw = getBenefitTierFromRaw;
30
+ exports.getTierFromAmount = getTierFromAmount;
31
+ exports.getTierName = getTierName;
32
+ exports.parseIFRAmount = parseIFRAmount;
33
+ exports.getTierFromRaw = getTierFromRaw;
34
+ exports.evaluateAccessRaw = evaluateAccessRaw;
35
+ const ethers_1 = require("ethers");
36
+ __exportStar(require("./benefits"), exports);
37
+ // ─── Constants ───────────────────────────────────────────────────────
38
+ exports.MAINNET_ADDRESSES = {
39
+ ifrToken: "0x77e99917Eca8539c62F509ED1193ac36580A6e7B",
40
+ ifrLock: "0x769928aBDfc949D0718d8766a1C2d7dBb63954Eb",
41
+ /** CommitmentVault V1: price tranches never unlock (CV-01); existing time tranches unlockable. Do not create new locks here. */
42
+ commitmentVault: "0x0719d9eb28dF7f5e63F91fAc4Bbb2d579C4F73d3",
43
+ /** CommitmentVault V2: TIME_ONLY; new locks after Governance proposal #17 (fee exemption) executes. */
44
+ commitmentVaultV2: "0x8efae0C85ad6d44C731cAEDA1cBC275904Fc7c8F",
45
+ /** LendingVault V1: retired, borrowing permanently disabled. */
46
+ lendingVault: "0x974305Ab0EC905172e697271C3d7d385194EB9DF",
47
+ builderRegistry: "0xdfe6636DA47F8949330697e1dC5391267CEf0EE3",
48
+ governance: "0xc43d48E7FDA576C5022d0670B652A622E8caD041",
49
+ };
50
+ exports.IFR_API = "https://copilot-api.ifrunit.tech";
51
+ /** @deprecated Use IFR_API. */
52
+ exports.RAILWAY_API = exports.IFR_API;
53
+ const DEFAULT_RPC = "https://ethereum-rpc.publicnode.com";
54
+ exports.IFR_DECIMALS = 9;
55
+ const TOKEN_ABI = [
56
+ "function balanceOf(address) view returns (uint256)",
57
+ "function decimals() view returns (uint8)",
58
+ "function totalSupply() view returns (uint256)",
59
+ ];
60
+ const LOCK_ABI = [
61
+ "function lockedBalance(address) view returns (uint256)",
62
+ "function isLocked(address, uint256) view returns (bool)",
63
+ ];
64
+ const BUILDER_REGISTRY_ABI = [
65
+ "function isBuilder(address) view returns (bool)",
66
+ "function getBuilderCount() view returns (uint256)",
67
+ ];
68
+ exports.DEFAULT_TIERS = Object.freeze([
69
+ Object.freeze({ key: "bronze", name: "Bronze", minLocked: "1000" }),
70
+ Object.freeze({ key: "silver", name: "Silver", minLocked: "2500" }),
71
+ Object.freeze({ key: "gold", name: "Gold", minLocked: "5000" }),
72
+ Object.freeze({ key: "platinum", name: "Platinum", minLocked: "10000" }),
73
+ ]);
74
+ /** Validates a tier list: non-empty, unique keys, strictly ascending exact 9-decimal thresholds. */
75
+ function validateTiers(tiers) {
76
+ if (!Array.isArray(tiers) || tiers.length === 0)
77
+ throw new Error("tiers must be a non-empty array");
78
+ const keys = new Set();
79
+ let previous = -1n;
80
+ return tiers.map((tier) => {
81
+ if (!tier || typeof tier.key !== "string" || !tier.key || typeof tier.name !== "string" || !tier.name) {
82
+ throw new Error("each tier needs a non-empty key and name");
83
+ }
84
+ if (keys.has(tier.key))
85
+ throw new Error(`duplicate tier key: ${tier.key}`);
86
+ keys.add(tier.key);
87
+ const min = parseIFRAmount(tier.minLocked);
88
+ if (min <= previous)
89
+ throw new Error("tier thresholds must be strictly ascending");
90
+ previous = min;
91
+ return min;
92
+ });
93
+ }
94
+ /** Highest tier reached by an exact locked amount (9-decimal base units); no rounding. */
95
+ function getBenefitTierFromRaw(lockedRaw, tiers = exports.DEFAULT_TIERS) {
96
+ const locked = (0, ethers_1.getBigInt)(lockedRaw);
97
+ if (locked < 0n)
98
+ throw new Error("IFR raw amounts must be non-negative");
99
+ const mins = validateTiers(tiers);
100
+ let level = 0;
101
+ for (let i = 0; i < mins.length; i++)
102
+ if (locked >= mins[i])
103
+ level = i + 1;
104
+ const tier = level ? tiers[level - 1] : null;
105
+ return { level, key: tier ? tier.key : null, name: tier ? tier.name : "None", lockedRaw: locked.toString() };
106
+ }
107
+ // ─── Read errors ─────────────────────────────────────────────────────
108
+ /**
109
+ * Thrown when an on-chain read fails. The SDK never turns a failed read into 0 or false,
110
+ * so a caller cannot mistake an RPC outage for "no IFR locked" or "not a builder".
111
+ */
112
+ class IFRReadError extends Error {
113
+ constructor(read, cause) {
114
+ super(`IFR read failed: ${read}${cause instanceof Error && cause.message ? ` (${cause.message})` : ""}`);
115
+ this.name = "IFRReadError";
116
+ this.read = read;
117
+ this.cause = cause;
118
+ }
119
+ }
120
+ exports.IFRReadError = IFRReadError;
121
+ async function readOrThrow(read, call) {
122
+ try {
123
+ return await call();
124
+ }
125
+ catch (error) {
126
+ throw new IFRReadError(read, error);
127
+ }
128
+ }
129
+ // ─── Legacy access tiers (deprecated) ────────────────────────────────
130
+ /**
131
+ * @deprecated Legacy hold+lock access tiers (wallet balance + locked, 500 / 2,000 / 10,000).
132
+ * Not the project's benefit preset. Use `DEFAULT_TIERS` with `getBenefitTierFromRaw`. Removed in 1.0.
133
+ */
134
+ exports.TIER_THRESHOLDS = { TIER1: 500, TIER2: 2000, TIER3: 10000 };
135
+ /** @deprecated Legacy access tier names; use `DEFAULT_TIERS`. Removed in 1.0. */
136
+ exports.TIER_NAMES = ["None", "Basic", "Premium", "Pro"];
137
+ /** @deprecated Legacy hold+lock access tier; use `getBenefitTierFromRaw`. Removed in 1.0. */
138
+ function getTierFromAmount(amount) {
139
+ if (amount >= exports.TIER_THRESHOLDS.TIER3)
140
+ return 3;
141
+ if (amount >= exports.TIER_THRESHOLDS.TIER2)
142
+ return 2;
143
+ if (amount >= exports.TIER_THRESHOLDS.TIER1)
144
+ return 1;
145
+ return 0;
146
+ }
147
+ /** @deprecated Legacy access tier name; use `getBenefitTierFromRaw(...).name`. Removed in 1.0. */
148
+ function getTierName(tier) {
149
+ return exports.TIER_NAMES[tier] || "None";
150
+ }
151
+ function parseIFRAmount(value) {
152
+ const normalized = String(value);
153
+ if (!/^(0|[1-9]\d*)(\.\d{1,9})?$/.test(normalized)) {
154
+ throw new Error("IFR amount must be a non-negative decimal with at most 9 decimal places");
155
+ }
156
+ return (0, ethers_1.parseUnits)(normalized, exports.IFR_DECIMALS);
157
+ }
158
+ /** @deprecated Legacy hold+lock access tier; use `getBenefitTierFromRaw`. Removed in 1.0. */
159
+ function getTierFromRaw(amount) {
160
+ const raw = (0, ethers_1.getBigInt)(amount);
161
+ if (raw >= parseIFRAmount(exports.TIER_THRESHOLDS.TIER3))
162
+ return 3;
163
+ if (raw >= parseIFRAmount(exports.TIER_THRESHOLDS.TIER2))
164
+ return 2;
165
+ if (raw >= parseIFRAmount(exports.TIER_THRESHOLDS.TIER1))
166
+ return 1;
167
+ return 0;
168
+ }
169
+ function evaluateAccessRaw(balanceRaw, lockedRaw, requiredRaw) {
170
+ const balance = (0, ethers_1.getBigInt)(balanceRaw);
171
+ const locked = (0, ethers_1.getBigInt)(lockedRaw);
172
+ const required = (0, ethers_1.getBigInt)(requiredRaw);
173
+ if (balance < 0n || locked < 0n || required < 0n) {
174
+ throw new Error("IFR raw amounts must be non-negative");
175
+ }
176
+ const total = balance + locked;
177
+ const tier = getTierFromRaw(total);
178
+ return { hasAccess: total >= required, total, tier, tierName: getTierName(tier) };
179
+ }
180
+ // ─── IFRClient ───────────────────────────────────────────────────────
181
+ class IFRClient {
182
+ constructor(config = {}) {
183
+ this.TIER1 = exports.TIER_THRESHOLDS.TIER1;
184
+ this.TIER2 = exports.TIER_THRESHOLDS.TIER2;
185
+ this.TIER3 = exports.TIER_THRESHOLDS.TIER3;
186
+ if (config.network && config.network !== "mainnet") {
187
+ throw new Error("Sepolia addresses are not configured; IFRClient currently supports Mainnet only");
188
+ }
189
+ const rpcUrl = config.rpcUrl || DEFAULT_RPC;
190
+ this.apiUrl = config.apiUrl || exports.IFR_API;
191
+ this.provider = new ethers_1.JsonRpcProvider(rpcUrl);
192
+ this.token = new ethers_1.Contract(exports.MAINNET_ADDRESSES.ifrToken, TOKEN_ABI, this.provider);
193
+ this.lockContract = new ethers_1.Contract(exports.MAINNET_ADDRESSES.ifrLock, LOCK_ABI, this.provider);
194
+ this.registry = new ethers_1.Contract(exports.MAINNET_ADDRESSES.builderRegistry, BUILDER_REGISTRY_ABI, this.provider);
195
+ }
196
+ /** Check if wallet has sufficient IFR (balance + locked) */
197
+ async checkAccess(params) {
198
+ const { wallet, required, checkLocked = true } = params;
199
+ if (!(0, ethers_1.isAddress)(wallet))
200
+ throw new Error("Invalid wallet address");
201
+ const requiredRaw = parseIFRAmount(required);
202
+ const [balanceRaw, lockedRaw] = await Promise.all([
203
+ this.token.balanceOf(wallet),
204
+ checkLocked
205
+ ? readOrThrow("IFRLock.lockedBalance", () => this.lockContract.lockedBalance(wallet))
206
+ : Promise.resolve(0n),
207
+ ]);
208
+ const evaluated = evaluateAccessRaw(balanceRaw, lockedRaw, requiredRaw);
209
+ const balance = Number((0, ethers_1.formatUnits)(balanceRaw, exports.IFR_DECIMALS));
210
+ const locked = Number((0, ethers_1.formatUnits)(lockedRaw, exports.IFR_DECIMALS));
211
+ const total = Number((0, ethers_1.formatUnits)(evaluated.total, exports.IFR_DECIMALS));
212
+ const requiredFormatted = Number((0, ethers_1.formatUnits)(requiredRaw, exports.IFR_DECIMALS));
213
+ return {
214
+ hasAccess: evaluated.hasAccess,
215
+ balance,
216
+ locked,
217
+ total,
218
+ required: requiredFormatted,
219
+ tier: evaluated.tier,
220
+ tierName: evaluated.tierName,
221
+ balanceRaw: balanceRaw.toString(),
222
+ lockedRaw: lockedRaw.toString(),
223
+ totalRaw: evaluated.total.toString(),
224
+ requiredRaw: requiredRaw.toString(),
225
+ };
226
+ }
227
+ /**
228
+ * Benefit tier from IFR locked in IFRLock only (default preset Bronze/Silver/Gold/Platinum, or your
229
+ * own `tiers`). Fails closed: a failed IFRLock read throws instead of reporting 0.
230
+ */
231
+ async getBenefitTier(wallet, tiers = exports.DEFAULT_TIERS) {
232
+ if (!(0, ethers_1.isAddress)(wallet))
233
+ throw new Error("Invalid wallet address");
234
+ validateTiers(tiers);
235
+ const lockedRaw = await this.lockContract.lockedBalance(wallet);
236
+ return getBenefitTierFromRaw(lockedRaw, tiers);
237
+ }
238
+ /**
239
+ * @deprecated Legacy hold+lock access tier (0=none, 1=basic, 2=premium, 3=pro; balance + locked,
240
+ * 500 / 2,000 / 10,000). Use `getBenefitTier`. Removed in 1.0.
241
+ */
242
+ async getTier(wallet) {
243
+ if (!(0, ethers_1.isAddress)(wallet))
244
+ throw new Error("Invalid wallet address");
245
+ const [balanceRaw, lockedRaw] = await Promise.all([
246
+ this.token.balanceOf(wallet),
247
+ readOrThrow("IFRLock.lockedBalance", () => this.lockContract.lockedBalance(wallet)),
248
+ ]);
249
+ const balance = Number((0, ethers_1.formatUnits)(balanceRaw, exports.IFR_DECIMALS));
250
+ const locked = Number((0, ethers_1.formatUnits)(lockedRaw, exports.IFR_DECIMALS));
251
+ const totalRaw = (0, ethers_1.getBigInt)(balanceRaw) + (0, ethers_1.getBigInt)(lockedRaw);
252
+ const tier = getTierFromRaw(totalRaw);
253
+ return {
254
+ tier,
255
+ tierName: getTierName(tier),
256
+ balance,
257
+ locked,
258
+ balanceRaw: balanceRaw.toString(),
259
+ lockedRaw: lockedRaw.toString(),
260
+ totalRaw: totalRaw.toString(),
261
+ };
262
+ }
263
+ /** Get IFR wallet balance */
264
+ async getBalance(wallet) {
265
+ if (!(0, ethers_1.isAddress)(wallet))
266
+ throw new Error("Invalid wallet address");
267
+ const raw = await this.token.balanceOf(wallet);
268
+ return Number((0, ethers_1.formatUnits)(raw, exports.IFR_DECIMALS));
269
+ }
270
+ /** Get locked IFR balance */
271
+ async getLockedBalance(wallet) {
272
+ if (!(0, ethers_1.isAddress)(wallet))
273
+ throw new Error("Invalid wallet address");
274
+ const raw = await readOrThrow("IFRLock.lockedBalance", () => this.lockContract.lockedBalance(wallet));
275
+ return Number((0, ethers_1.formatUnits)(raw, exports.IFR_DECIMALS));
276
+ }
277
+ /** Check if address is a registered builder */
278
+ async isBuilder(address) {
279
+ if (!(0, ethers_1.isAddress)(address))
280
+ throw new Error("Invalid address");
281
+ return readOrThrow("BuilderRegistry.isBuilder", () => this.registry.isBuilder(address));
282
+ }
283
+ /** Get total IFR supply */
284
+ async getTotalSupply() {
285
+ const raw = await this.token.totalSupply();
286
+ return Number((0, ethers_1.formatUnits)(raw, exports.IFR_DECIMALS));
287
+ }
288
+ /** REST API check — no ethers dependency needed on caller side */
289
+ static async apiCheck(params) {
290
+ if (!(0, ethers_1.isAddress)(params.wallet))
291
+ throw new Error("Invalid wallet address");
292
+ const required = params.required ?? 1000;
293
+ parseIFRAmount(required);
294
+ const url = new URL("/api/ifr/check", params.apiUrl || exports.IFR_API);
295
+ url.search = new URLSearchParams({ wallet: params.wallet, required: String(required) }).toString();
296
+ const resp = await fetch(url.toString());
297
+ if (!resp.ok)
298
+ throw new Error(`API error: ${resp.status}`);
299
+ return resp.json();
300
+ }
301
+ }
302
+ exports.IFRClient = IFRClient;
303
+ exports.default = IFRClient;
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "ifr-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "IFR Protocol SDK - exact access checks and signer-neutral Benefits checkout",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "engines": {
20
+ "node": ">=20 <23"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "test": "npm run build && node test/sdk.test.cjs",
25
+ "test:package": "npm run build && node test/package-consumer.cjs",
26
+ "prepublishOnly": "npm test && npm run test:package"
27
+ },
28
+ "keywords": [
29
+ "ifr",
30
+ "web3",
31
+ "access-control",
32
+ "lock",
33
+ "defi",
34
+ "ethereum"
35
+ ],
36
+ "license": "MIT",
37
+ "author": "Inferno Protocol contributors",
38
+ "homepage": "https://github.com/NeaBouli/inferno/tree/main/apps/sdk#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/NeaBouli/inferno/issues"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/NeaBouli/inferno.git",
45
+ "directory": "apps/sdk"
46
+ },
47
+ "dependencies": {
48
+ "ethers": "^6.15.0"
49
+ },
50
+ "devDependencies": {
51
+ "typescript": "^5.0.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "provenance": true
56
+ }
57
+ }