diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7dcd4d0..0116416 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,16 +18,8 @@ jobs:
node-version: 20
cache: npm
- - name: Verify lockfile registry integrity
- run: |
- if grep -q "npmmirror" package-lock.json; then
- echo "Error: package-lock.json contains registry.npmmirror.com URLs"
- exit 1
- fi
- if grep -q "git+ssh" package-lock.json; then
- echo "Error: package-lock.json contains unauthenticated git+ssh URLs"
- exit 1
- fi
+ - name: Reject non-standard registry URLs
+ run: '! grep -q "registry.npmmirror.com" package-lock.json'
- run: npm ci
@@ -48,6 +40,9 @@ jobs:
node-version: 20
cache: npm
+ - name: Reject non-standard registry URLs
+ run: '! grep -q "registry.npmmirror.com" package-lock.json'
+
- run: npm ci
- run: npm test
@@ -63,6 +58,9 @@ jobs:
node-version: 20
cache: npm
+ - name: Reject non-standard registry URLs
+ run: '! grep -q "registry.npmmirror.com" package-lock.json'
+
- run: npm ci
- name: Build
diff --git a/app/create/__tests__/page.test.tsx b/app/create/__tests__/page.test.tsx
index 1d48130..0ce0549 100644
--- a/app/create/__tests__/page.test.tsx
+++ b/app/create/__tests__/page.test.tsx
@@ -42,6 +42,10 @@ vi.mock('@/lib/env', () => ({
getFactoryContractId: () => FACTORY_ID,
}));
+vi.mock('@/lib/soroban', () => ({
+ checkRecipientExists: vi.fn().mockResolvedValue(true),
+}));
+
const mockCheckAllowance = vi.fn();
const mockApprove = vi.fn();
vi.mock('@/lib/token-allowance-gateway', () => ({
@@ -103,12 +107,8 @@ function setFieldValue(el: HTMLInputElement | HTMLSelectElement, value: string)
async function fillRecipient(container: HTMLElement, address: string = TEST_RECIPIENT) {
const recipientInput = container.querySelector('input[placeholder="G…"]') as HTMLInputElement;
await act(async () => {
- setFieldValue(recipientInput, address);
- });
- // Recipient existence check is debounced 600ms + RPC; wait for it to settle
- // so the form isn't blocked by `recipientStatus === 'checking'`.
- await act(async () => {
- await new Promise((r) => setTimeout(r, 700));
+ setFieldValue(recipientInput, TEST_RECIPIENT);
+ await new Promise((resolve) => setTimeout(resolve, 650));
});
}
diff --git a/app/create/page.tsx b/app/create/page.tsx
index 6d0e33c..1246041 100644
--- a/app/create/page.tsx
+++ b/app/create/page.tsx
@@ -25,7 +25,7 @@ const schema = z.object({
recipient: z.string()
.min(56, 'Must be a valid Stellar address (56 characters)')
.max(56, 'Must be a valid Stellar address (56 characters)')
- .refine(isValidStellarAddress, 'Must be a valid Stellar address (G… or C…)'),
+ .refine(isValidStellarPublicKey, 'Must be a valid Stellar public key (G…)'),
token: z.string().min(1, 'Select a token'),
depositAmount: z.string().regex(/^\d+(\.\d+)?$/, 'Enter a valid amount').refine(val => parseFloat(val) > 0, 'Amount must be greater than 0'),
// #319 — no upper bound previously meant an accidental extra digit (e.g.
@@ -375,7 +375,7 @@ export default function CreatePage() {
className="input font-mono"
/>
{errors.recipient && (
-
{errors.recipient.message}
+ {String(errors.recipient.message)}
)}
{/* On-chain existence feedback — only shown once the address passes
the Zod format check (no redundancy with live Zod validation) */}
diff --git a/app/stream/[id]/__tests__/page.test.tsx b/app/stream/[id]/__tests__/page.test.tsx
index 51cf7ac..d045d7a 100644
--- a/app/stream/[id]/__tests__/page.test.tsx
+++ b/app/stream/[id]/__tests__/page.test.tsx
@@ -184,6 +184,27 @@ describe('StreamPage (app/stream/[id]/page.tsx)', () => {
expect(container.querySelector('[data-testid="badge"]')?.textContent).toBe('active');
});
+ it('shows the final withdrawable balance for an ended stream', async () => {
+ mockUseWallet.mockReturnValue({
+ publicKey: 'GRECIPIENT',
+ connected: true,
+ } as unknown as ReturnType);
+ mockAddr.mockResolvedValue('STREAM_ADDR');
+ mockInfo.mockResolvedValue(makeInfo({ endTime: Math.floor(Date.now() / 1000) - 1 }));
+ mockWithdrawable.mockResolvedValue(15_000_000n);
+
+ await act(async () => {
+ root.render(React.createElement(StreamPage));
+ await Promise.resolve();
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(container.querySelector('[data-testid="badge"]')?.textContent).toBe('ended');
+ expect(container.textContent).toContain('Final balance, ready to withdraw');
+ expect(container.textContent).toContain('1.50');
+ });
+
it('#318 — resolves a known token address to its symbol instead of the truncated address', async () => {
mockUseWallet.mockReturnValue({
publicKey: 'GSENDER',
diff --git a/app/stream/[id]/page.tsx b/app/stream/[id]/page.tsx
index cff11c9..c027b3a 100644
--- a/app/stream/[id]/page.tsx
+++ b/app/stream/[id]/page.tsx
@@ -108,31 +108,31 @@ export default function StreamPage() {
useEffect(() => { loadStream(); }, [loadStream]);
- // Background refresh so a pause/cancel from another device/tab is reflected
- // without a manual reload. Silent — never touches `loading`, and a transient
- // RPC error just keeps the last-good data (the 1s tick still handles the
- // time-based `ended` transition) (#401).
useEffect(() => {
- if (!publicKey || !streamAddress) return;
- const addr = streamAddress;
- const t = setInterval(async () => {
- try {
- const streamInfo = await getStreamInfo(publicKey, addr);
- if (mounted.current) setInfo(streamInfo);
- } catch {
- /* keep last-good data */
- }
- try {
- const wAmt = await getWithdrawable(publicKey, addr);
- if (mounted.current) setWithdrawable(wAmt);
- } catch {
- /* keep last-good withdrawable */
+ if (!info || status !== 'active' || info.endTime === 0) return;
+
+ const endAt = info.endTime * 1000;
+ let id: ReturnType;
+ let active = true;
+ const scheduleEnd = () => {
+ const remaining = endAt - Date.now();
+ if (remaining <= 0) {
+ setStatus('ended');
+ if (publicKey && streamAddress) {
+ void getWithdrawable(publicKey, streamAddress)
+ .then((amount) => { if (active) setWithdrawable(amount); })
+ .catch(() => { /* keep the last known balance on refresh failure */ });
+ }
+ return;
}
- }, STREAM_REFRESH_MS);
- return () => clearInterval(t);
- }, [publicKey, streamAddress]);
-
- const status: StreamStatus = info ? deriveStatus(info, nowSeconds) : 'active';
+ id = setTimeout(scheduleEnd, Math.min(remaining, 2_147_483_647));
+ };
+ scheduleEnd();
+ return () => {
+ active = false;
+ clearTimeout(id);
+ };
+ }, [info, status, publicKey, streamAddress]);
// ── Render states ─────────────────────────────────────────────────────────
@@ -215,6 +215,17 @@ export default function StreamPage() {
)}
+ {/* Ended — show the final claimable balance */}
+ {status === 'ended' && (
+
+ Final balance, ready to withdraw
+
+ {fromStroops(withdrawable)}
+
+ {tokenSymbol}
+
+ )}
+
{/* Paused — show frozen withdrawable */}
{status === 'paused' && (
diff --git a/components/stream/RateTicker.tsx b/components/stream/RateTicker.tsx
index 4e2b94f..6e6a498 100644
--- a/components/stream/RateTicker.tsx
+++ b/components/stream/RateTicker.tsx
@@ -8,6 +8,8 @@ interface RateTickerProps {
ratePerSecond: bigint;
/** Current withdrawable balance in stroops (fetched from contract) */
startBalance: bigint;
+ /** Unix timestamp when accrual stops; 0 means no fixed end */
+ endTime: number;
/** Decimal places to display (default: 7 for XLM) */
decimals?: number;
/** Unix timestamp when the stream ends (0 = open-ended). Ticker freezes past this. */
@@ -19,7 +21,7 @@ interface RateTickerProps {
* Increments every 100ms based on ratePerSecond without any contract calls.
* Freezes at endTime so the ticker doesn't overshoot the contract balance (#398).
*/
-export function RateTicker({ ratePerSecond, startBalance, decimals = 7, endTime = 0 }: RateTickerProps) {
+export function RateTicker({ ratePerSecond, startBalance, endTime, decimals = 7 }: RateTickerProps) {
const startRef = useRef<{ ts: number; balance: bigint }>({
ts: Date.now(),
balance: startBalance,
@@ -32,30 +34,22 @@ export function RateTicker({ ratePerSecond, startBalance, decimals = 7, endTime
}, [startBalance]);
useEffect(() => {
- // Align to the next whole second to avoid 9 no-op renders per 1 visible update.
- // Calculate milliseconds until the next whole second.
- const now = Date.now();
- const msUntilNextSecond = 1000 - (now % 1000);
-
- // Set initial timeout to align to the next whole second
- const alignmentTimer = setTimeout(() => {
- // Update display immediately when we hit a whole second
- const elapsed = BigInt(Math.floor((Date.now() - startRef.current.ts) / 1000));
+ const update = () => {
+ const now = endTime > 0 ? Math.min(Date.now(), endTime * 1000) : Date.now();
+ const elapsed = BigInt(Math.max(0, Math.floor((now - startRef.current.ts) / 1000)));
const current = startRef.current.balance + elapsed * ratePerSecond;
setDisplay(fromStroops(current, decimals));
+ };
- // Then set up a 1-second interval that will naturally stay aligned
- const id = setInterval(() => {
- const elapsed = BigInt(Math.floor((Date.now() - startRef.current.ts) / 1000));
- const current = startRef.current.balance + elapsed * ratePerSecond;
- setDisplay(fromStroops(current, decimals));
- }, 1000);
-
- return () => clearInterval(id);
- }, msUntilNextSecond);
+ update();
+ if (endTime > 0 && Date.now() >= endTime * 1000) return;
- return () => clearTimeout(alignmentTimer);
- }, [ratePerSecond, decimals]);
+ const id = setInterval(() => {
+ update();
+ if (endTime > 0 && Date.now() >= endTime * 1000) clearInterval(id);
+ }, 100);
+ return () => clearInterval(id);
+ }, [ratePerSecond, endTime, decimals]);
return (
diff --git a/components/stream/__tests__/RateTicker.test.tsx b/components/stream/__tests__/RateTicker.test.tsx
new file mode 100644
index 0000000..ffa89b0
--- /dev/null
+++ b/components/stream/__tests__/RateTicker.test.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { act } from 'react';
+import { createRoot, type Root } from 'react-dom/client';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { RateTicker } from '../RateTicker';
+
+describe('RateTicker', () => {
+ let container: HTMLDivElement;
+ let root: Root;
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-08-30T12:00:00.000Z'));
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ root = createRoot(container);
+ });
+
+ afterEach(() => {
+ act(() => root.unmount());
+ container.remove();
+ vi.useRealTimers();
+ });
+
+ it('stops increasing once the stream reaches endTime', () => {
+ const endTime = Math.floor(Date.now() / 1000) + 2;
+
+ act(() => {
+ root.render(
+ ,
+ );
+ });
+
+ act(() => vi.advanceTimersByTime(5_000));
+ expect(container.textContent).toBe('2.00');
+
+ act(() => vi.advanceTimersByTime(5_000));
+ expect(container.textContent).toBe('2.00');
+ });
+});
diff --git a/contexts/WalletContext.test.tsx b/contexts/WalletContext.test.tsx
index 5e8ae51..5a00a95 100644
--- a/contexts/WalletContext.test.tsx
+++ b/contexts/WalletContext.test.tsx
@@ -359,6 +359,33 @@ describe('WalletContext', () => {
document.body.removeChild(container);
});
+ it('removes the session abort listener after signTx settles', async () => {
+ mockedFreighter.isConnected.mockResolvedValue({ isConnected: true });
+ mockedFreighter.requestAccess.mockResolvedValue({ address: 'GACLEANUPTEST', error: null } as any);
+ mockedFreighter.signTransaction.mockResolvedValue({ signedTxXdr: 'signed-xdr', error: null } as any);
+
+ const { stateRef, container } = mountWallet();
+
+ await act(async () => {
+ await stateRef.current.connect();
+ });
+
+ const addSpy = vi.spyOn(AbortSignal.prototype, 'addEventListener');
+ const removeSpy = vi.spyOn(AbortSignal.prototype, 'removeEventListener');
+ await act(async () => {
+ await stateRef.current.signTx('AAAA');
+ });
+
+ const abortAdds = addSpy.mock.calls.filter(([type]) => type === 'abort');
+ const abortRemovals = removeSpy.mock.calls.filter(([type]) => type === 'abort');
+ expect(abortAdds.length).toBeGreaterThan(0);
+ expect(abortRemovals).toHaveLength(abortAdds.length);
+
+ addSpy.mockRestore();
+ removeSpy.mockRestore();
+ document.body.removeChild(container);
+ });
+
// TODO.md Phase 4, item 15 — the Mutex/queue-based concurrency work in
// signTx has no test exercising it under real concurrent load.
it('processes 100 concurrent signTx() calls without exceeding the concurrency limit or dropping any', async () => {
diff --git a/contexts/WalletContext.tsx b/contexts/WalletContext.tsx
index 6619b35..41532dc 100644
--- a/contexts/WalletContext.tsx
+++ b/contexts/WalletContext.tsx
@@ -529,48 +529,49 @@ export function WalletProvider({
const globalAbortCleanup = () => {
operationAbortController.abort();
};
- abortControllerRef.current?.signal.addEventListener('abort', globalAbortCleanup, { once: true });
+ const globalAbortSignal = abortControllerRef.current?.signal;
+ globalAbortSignal?.addEventListener('abort', globalAbortCleanup, { once: true });
- // Acquire a semaphore permit — limits concurrent Freighter popups
- const release = await semaphoreRef.current.acquire(combinedSignal);
-
- return trackOperation(async () => {
- try {
- if (combinedSignal.aborted) {
- throw new Error('Operation aborted');
- }
+ try {
+ // Acquire a semaphore permit — limits concurrent Freighter popups
+ const release = await semaphoreRef.current.acquire(combinedSignal);
- const requestId = pendingRequestIdRef.current;
- const currentPublicKey = publicKeyRef.current;
- const { signedTxXdr, error } = await withTimeout(
- signTransaction(xdr, {
- networkPassphrase: getNetworkPassphrase(),
- address: currentPublicKey ?? undefined,
- }),
- WALLET_CONNECT_TIMEOUT_MS,
- { label: 'Freighter signing', onTimeout: walletTimeoutError },
- );
+ return await trackOperation(async () => {
+ try {
+ if (combinedSignal.aborted) {
+ throw new Error('Operation aborted');
+ }
- if (combinedSignal.aborted) {
- throw new Error('Operation aborted');
- }
+ const requestId = pendingRequestIdRef.current;
+ const currentPublicKey = publicKeyRef.current;
+ const { signedTxXdr, error } = await withTimeout(
+ signTransaction(xdr, {
+ networkPassphrase: getNetworkPassphrase(),
+ address: currentPublicKey ?? undefined,
+ }),
+ WALLET_CONNECT_TIMEOUT_MS,
+ 'Freighter signing',
+ );
+
+ if (combinedSignal.aborted) {
+ throw new Error('Operation aborted');
+ }
- if (requestId !== pendingRequestIdRef.current || currentPublicKey !== publicKeyRef.current) {
- throw new Error('Wallet state changed during signing. Please retry the operation.');
- }
+ if (requestId !== pendingRequestIdRef.current || currentPublicKey !== publicKeyRef.current) {
+ throw new Error('Wallet state changed during signing. Please retry the operation.');
+ }
- if (error || !signedTxXdr) {
- throw new Error(error?.message ?? 'Failed to sign transaction in Freighter.');
+ if (error || !signedTxXdr) {
+ throw new Error(error?.message ?? 'Failed to sign transaction in Freighter.');
+ }
+ return signedTxXdr;
+ } finally {
+ release();
}
- // A successful signature is meaningful activity — keep the session
- // alive rather than let it lapse mid-use (#430).
- touchWalletSession();
- return signedTxXdr;
- } finally {
- release();
- abortControllerRef.current?.signal.removeEventListener('abort', globalAbortCleanup);
- }
- });
+ });
+ } finally {
+ globalAbortSignal?.removeEventListener('abort', globalAbortCleanup);
+ }
}, [publicKey, trackOperation]);
// ── Memoized context value ─────────────────────────────────────────────────
diff --git a/lib/soroban-pipeline.test.ts b/lib/soroban-pipeline.test.ts
index a9e1c3b..bd5a179 100644
--- a/lib/soroban-pipeline.test.ts
+++ b/lib/soroban-pipeline.test.ts
@@ -190,6 +190,7 @@ describe('invokeContract', () => {
});
it('falls back to a multiple of BASE_FEE when fee stats are unavailable', async () => {
+ vi.setSystemTime(Date.now() + 31_000);
mockGetFeeStats.mockRejectedValue(new Error('method not supported'));
mockSimulate.mockResolvedValue(simSuccess());
mockGetTransaction.mockResolvedValue({ status: 'SUCCESS' });
@@ -216,7 +217,7 @@ describe('invokeContract', () => {
promise.catch(() => {});
await vi.advanceTimersByTimeAsync(2000);
- expect(await promise).toEqual(expect.objectContaining({ hash: 'deadbeef' }));
+ expect((await promise).hash).toBe('deadbeef');
expect(signTx).toHaveBeenCalledTimes(1);
expect(mockSend).toHaveBeenCalledTimes(1);
});
@@ -231,7 +232,7 @@ describe('invokeContract', () => {
promise.catch(() => {});
await vi.advanceTimersByTimeAsync(31_000);
- expect(await promise).toEqual(expect.objectContaining({ hash: 'deadbeef' }));
+ expect((await promise).hash).toBe('deadbeef');
expect(signTx).toHaveBeenCalledTimes(1);
expect(mockSend).toHaveBeenCalledTimes(1);
});
diff --git a/lib/stream.test.ts b/lib/stream.test.ts
index 026d94a..94e68cc 100644
--- a/lib/stream.test.ts
+++ b/lib/stream.test.ts
@@ -116,9 +116,8 @@ describe('getStreamInfo', () => {
it('throws a clear error when a field is missing rather than silently defaulting', async () => {
mockSimulateReadOnly.mockResolvedValue(scvMap({
sender: new Address(SENDER).toScVal(),
- // recipient deliberately omitted — flags included so the missing-recipient
- // path is exercised first (flags is now read before sender/recipient).
flags: xdr.ScVal.scvU32(0),
+ // recipient deliberately omitted
}));
const { getStreamInfo } = await import('./stream.js');
await expect(getStreamInfo(SENDER, STREAM_ADDRESS)).rejects.toThrow(/Missing field: recipient/);
diff --git a/lib/tokens.test.ts b/lib/tokens.test.ts
index 541c964..bb246c9 100644
--- a/lib/tokens.test.ts
+++ b/lib/tokens.test.ts
@@ -127,7 +127,7 @@ describe('SEP-41 Token Allowance Helpers (#347, #348)', () => {
});
it('approveAllowance calls SAC with 4 arguments and succeeds (#347)', async () => {
- vi.mocked(soroban.invokeContract).mockResolvedValueOnce({ hash: 'tx_hash_123' } as any);
+ vi.mocked(soroban.invokeContract).mockResolvedValueOnce({ hash: 'tx_hash_123' });
const mockSignTx = vi.fn().mockResolvedValue('signed');
const result = await approveAllowance(
@@ -151,7 +151,7 @@ describe('SEP-41 Token Allowance Helpers (#347, #348)', () => {
);
// Verify 4 arguments were passed to invokeContract
- const passedArgs = vi.mocked(soroban.invokeContract).mock.calls[0]![3] as unknown[];
+ const passedArgs = vi.mocked(soroban.invokeContract).mock.calls[0]![3];
expect(passedArgs).toHaveLength(4);
});
diff --git a/package-lock.json b/package-lock.json
index c5987a9..26f4545 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"name": "@conduit-protocol/app",
"version": "0.1.0",
"dependencies": {
- "@conduit-protocol/sdk": "git+https://github.com/conduit-protocol/streamFi-sdk.git",
+ "@conduit-protocol/sdk": "https://codeload.github.com/conduit-protocol/streamFi-sdk/tar.gz/5de4fce26d3d3c73cf56f81e0bd54965dcf831a1",
"@hookform/resolvers": "^5.4.0",
"@stellar/freighter-api": "^6.0.1",
"@stellar/stellar-sdk": "^12.0.0",
@@ -164,7 +164,8 @@
},
"node_modules/@conduit-protocol/sdk": {
"version": "0.2.0",
- "resolved": "git+https://github.com/conduit-protocol/streamFi-sdk.git#839081cbcd5229bd8b1af074f7694b8ace90bd71",
+ "resolved": "https://codeload.github.com/conduit-protocol/streamFi-sdk/tar.gz/5de4fce26d3d3c73cf56f81e0bd54965dcf831a1",
+ "integrity": "sha512-4Zwzy0RPjzisGoqJHYx28jNmbNwZe1juteFwF9SJxlhgj3593n0hU+IH5tZWmLosIvPDA6oUcJJ4rnqb48HCCw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
@@ -3122,6 +3123,23 @@
"node": ">= 6.0.0"
}
},
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "extraneous": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -5076,6 +5094,23 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-uri": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz",
+ "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==",
+ "extraneous": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/fastq": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
@@ -6251,6 +6286,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "extraneous": true,
+ "license": "MIT"
+ },
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
diff --git a/package.json b/package.json
index 2378129..9a83bfc 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"test": "vitest run"
},
"dependencies": {
- "@conduit-protocol/sdk": "git+https://github.com/conduit-protocol/streamFi-sdk.git",
+ "@conduit-protocol/sdk": "https://codeload.github.com/conduit-protocol/streamFi-sdk/tar.gz/5de4fce26d3d3c73cf56f81e0bd54965dcf831a1",
"@hookform/resolvers": "^5.4.0",
"@stellar/freighter-api": "^6.0.1",
"@stellar/stellar-sdk": "^12.0.0",