Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"Bash(pnpm:*)",
"Read(.env)",
"Read(.env.*)",
"Read(supabase/.env)"
"Read(packages/wallet-sdk/db/supabase/.env)"
]
},
"hooks": {
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/supabase-database/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ Expert guidance for Postgres database work in a Supabase environment.
- Write all SQL in lowercase
- Always enable RLS on new tables
- Separate RLS policies per operation (select/insert/update/delete) and per role (anon/authenticated)
- Migration files: `YYYYMMDDHHmmss_short_description.sql` in `packages/wallet-sdk/supabase/migrations/`
- Migration files: `YYYYMMDDHHmmss_short_description.sql` in `packages/wallet-sdk/db/supabase/migrations/`
- Functions default to `SECURITY INVOKER` with `set search_path = ''`
- Use fully qualified names (e.g., `public.table_name`) in functions
2 changes: 1 addition & 1 deletion .claude/skills/supabase-database/references/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This project uses the migrations provided by the Supabase CLI.

## Creating a migration file

Create migration files inside `packages/wallet-sdk/supabase/migrations/`.
Create migration files inside `packages/wallet-sdk/db/supabase/migrations/`.

The file MUST be named in the format `YYYYMMDDHHmmss_short_description.sql` with proper casing for months, minutes, and seconds in UTC time:

Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ on:
pull_request:

env:
SELF_SIGNED_CERT_PATH: "../../../certs/ci-localhost-cert.pem"
SELF_SIGNED_CERT_KEY_PATH: "../../../certs/ci-localhost-key.pem"
SELF_SIGNED_CERT_PATH: "../../../../certs/ci-localhost-cert.pem"
SELF_SIGNED_CERT_KEY_PATH: "../../../../certs/ci-localhost-key.pem"

jobs:
code-checks:
Expand All @@ -28,7 +28,7 @@ jobs:
MIGRATIONS_CHANGED=false
CLI_VERSION_CHANGED=false

if ! git diff --quiet origin/${{ github.base_ref }}..HEAD -- packages/wallet-sdk/supabase/migrations/; then
if ! git diff --quiet origin/${{ github.base_ref }}..HEAD -- packages/wallet-sdk/db/supabase/migrations/; then
MIGRATIONS_CHANGED=true
fi

Expand All @@ -50,9 +50,9 @@ jobs:
fi

echo "Checking types..."
bun supabase db start --workdir packages/wallet-sdk
bun supabase gen types typescript --local --schema wallet --workdir packages/wallet-sdk > packages/wallet-sdk/supabase/database.types.ts
if ! git diff --ignore-space-at-eol --exit-code --quiet packages/wallet-sdk/supabase/database.types.ts; then
bun supabase db start --workdir packages/wallet-sdk/db
bun supabase gen types typescript --local --schema wallet --workdir packages/wallet-sdk/db > packages/wallet-sdk/db/supabase/database.types.ts
if ! git diff --ignore-space-at-eol --exit-code --quiet packages/wallet-sdk/db/supabase/database.types.ts; then
echo "Detected uncommitted changes in database types. This means that db was changed but types were not updated. To fix regenerate the types and commit the changes. See missing types below:"
git diff
exit 1
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,18 +178,18 @@ hosted on Supabase. Sensitive data is encrypted with the key from the Open Secre
logged-in user. While developing locally, the local Supabase stack is used. To start it, run `bun run supabase start` command.
To stop it, run `bun run supabase stop` command. The start command will start the database and realtime service, plus other
services useful for development, like Supabase Studio. You can use Supabase Studio to inspect the database and run queries.
Supabase is configured in the `packages/wallet-sdk/supabase/config.toml` file.
Supabase is configured in the `packages/wallet-sdk/db/supabase/config.toml` file.

### Database migrations

Schema changes to the Postgres database should be done using migrations. Migrations are stored in the `packages/wallet-sdk/supabase/migrations`
Schema changes to the Postgres database should be done using migrations. Migrations are stored in the `packages/wallet-sdk/db/supabase/migrations`
folder. Always try to make the schema changes in a backward compatible way.

Database migrations can be done in two ways:
1. Using Supabase Studio. With this approach you can make db changes directly to your local database in the Studio UI
and then run `bun supabase db diff --file <MIGRATION_NAME>` to create a migration file for the changes.
2. Using `bun supabase migration new <MIGRATION_NAME>`. This command will create a new empty migration file in the
`packages/wallet-sdk/supabase/migrations` folder where you can then write the SQL commands to make the changes. To apply the migration to
`packages/wallet-sdk/db/supabase/migrations` folder where you can then write the SQL commands to make the changes. To apply the migration to
the local database run `bun supabase db push`.

To keep the db TypeScript types in sync with the database schema run `bun run db:generate-types` command. If you forget
Expand Down
4 changes: 2 additions & 2 deletions apps/web-wallet-e2e/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ VITE_SENTRY_HOST='o4509706567680000.ingest.us.sentry.io'
VITE_SENTRY_PROJECT_ID='4509707316690944'
VITE_SENTRY_DSN='https://3e5837ba4db251e806915155170ef71b@${VITE_SENTRY_HOST}/${VITE_SENTRY_PROJECT_ID}'
VITE_ENVIRONMENT='local'
SELF_SIGNED_CERT_PATH='../../../certs/localhost-cert.pem'
SELF_SIGNED_CERT_KEY_PATH='../../../certs/localhost-key.pem'
SELF_SIGNED_CERT_PATH='../../../../certs/localhost-cert.pem'
SELF_SIGNED_CERT_KEY_PATH='../../../../certs/localhost-key.pem'
NODE_TLS_REJECT_UNAUTHORIZED=0
LNURL_SERVER_SPARK_MNEMONIC='half vanish taxi again drill detail neglect park harsh setup involve dawn'
LNURL_SERVER_ENCRYPTION_KEY="a8861cc3e5b3caf5573fbba2da2851a4e608a836eef10074be36ae0ca147b518"
Expand Down
23 changes: 7 additions & 16 deletions apps/web-wallet/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { configure } from '@agicash/opensecret';
import {
configureFeatureFlags,
ensureBreezWasm,
Expand All @@ -15,6 +14,13 @@ import { HydratedRouter } from 'react-router/dom';
import { getEnvironment, isServedLocally } from './environment';
import { agicashDbClient } from './features/agicash-db/database.client';
import { loadFeatureFlags } from './features/shared/feature-flags';
// Side-effect import: evaluating this module constructs the SDK, which
// configures Open Secret. loadFeatureFlags() below relies on that — for a
// returning user it fetches user-targeted flags with a token minted through
// Open Secret — so the construction has to run before the body. Temporary: a
// later slice constructs the SDK explicitly at boot and moves feature flags
// onto the instance, dropping this ordering dependency (PR #1166).
import './features/shared/sdk.client';
import { registerMoneyDevToolsFormatter } from './lib/money-devtools-formatter';
import { getTracesSampleRate, sanitizeUrl } from './tracing-utils';

Expand All @@ -23,21 +29,6 @@ if (process.env.NODE_ENV === 'development') {
registerMoneyDevToolsFormatter();
}

const openSecretApiUrl = import.meta.env.VITE_OPEN_SECRET_API_URL ?? '';
if (!openSecretApiUrl) {
throw new Error('VITE_OPEN_SECRET_API_URL is not set');
}

const openSecretClientId = import.meta.env.VITE_OPEN_SECRET_CLIENT_ID ?? '';
if (!openSecretClientId) {
throw new Error('VITE_OPEN_SECRET_CLIENT_ID is not set');
}

configure({
apiUrl: openSecretApiUrl,
clientId: openSecretClientId,
});

// Start Breez WASM fetch/compile as early as possible so it overlaps with
// hydration, Sentry init, and the auth query — by the time the _protected
// middleware awaits it, init is often already done.
Expand Down
29 changes: 1 addition & 28 deletions apps/web-wallet/app/features/agicash-db/database.client.ts
Comment thread
jbojcic1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,36 +1,9 @@
import type { Database } from '@agicash/wallet-sdk/temporary';
import { createClient } from '@supabase/supabase-js';
import { supabaseAnonKey, supabaseUrl } from '~/features/shared/sdk.client';
import { SupabaseRealtimeManager } from '~/lib/supabase';
import { getSupabaseSessionToken } from './supabase-session';

const getSupabaseUrl = () => {
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL ?? '';
if (!supabaseUrl) {
throw new Error('VITE_SUPABASE_URL is not set');
}

if (
supabaseUrl.includes('127.0.0.1') &&
typeof window !== 'undefined' &&
window.location.protocol === 'https:' &&
(window.location.hostname.endsWith('.local') ||
window.location.hostname.startsWith('192.168.') ||
window.location.hostname.startsWith('10.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(window.location.hostname))
) {
return supabaseUrl.replace('127.0.0.1', window.location.hostname);
}

return supabaseUrl;
};

const supabaseUrl = getSupabaseUrl();

const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY ?? '';
if (!supabaseAnonKey) {
throw new Error('VITE_SUPABASE_ANON_KEY is not set');
}

/**
* The client-side Supabase database client.
* If you need to use a client on the server, which bypasses RLS, use `agicashDbServer` instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import {
sumProofs,
} from '@agicash/cashu';
import type { Money } from '@agicash/money';
import {
type LongTimeout,
clearLongTimeout,
setLongTimeout,
} from '@agicash/utils';
import type {
CashuAccount,
CashuReceiveQuote,
Expand Down Expand Up @@ -34,11 +39,6 @@ import {
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useOnMeltQuoteStateChange } from '~/lib/cashu/melt-quote-subscription';
import { MintQuoteSubscriptionManager } from '~/lib/cashu/mint-quote-subscription-manager';
import {
type LongTimeout,
clearLongTimeout,
setLongTimeout,
} from '~/lib/timeout';
import { useLatest } from '~/lib/use-latest';
import { withRetry } from '~/lib/with-retry';
import {
Expand Down
15 changes: 11 additions & 4 deletions apps/web-wallet/app/features/shared/auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { jwtDecode } from 'jwt-decode';
import { safeJwtDecode } from '@agicash/utils';

/** Check if the user is logged in by verifying localStorage tokens. */
/**
* Check if the user is logged in by verifying localStorage tokens. A corrupt
* stored token counts as logged out — this feeds the DB client's token
* getter, so it must never throw into every query.
*
* Temporary leak: reads Open Secret's storage keys directly until the late
* SDK-migration steps retire the web's own DB client.
*/
export const isLoggedIn = (): boolean => {
const accessToken = window.localStorage.getItem('access_token');
const refreshToken = window.localStorage.getItem('refresh_token');
if (!accessToken || !refreshToken) {
return false;
}
const decoded = jwtDecode(refreshToken);
return !!decoded.exp && decoded.exp * 1000 > Date.now();
const decoded = safeJwtDecode(refreshToken);
return !!decoded?.exp && decoded.exp * 1000 > Date.now();
};
83 changes: 83 additions & 0 deletions apps/web-wallet/app/features/shared/sdk.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { browserStorage } from '@agicash/opensecret';
import { AgicashSdk } from '@agicash/wallet-sdk';
import { breezApiKey } from '~/lib/breez';

const getSupabaseUrl = () => {
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL ?? '';
if (!supabaseUrl) {
throw new Error('VITE_SUPABASE_URL is not set');
}

if (
supabaseUrl.includes('127.0.0.1') &&
typeof window !== 'undefined' &&
window.location.protocol === 'https:' &&
(window.location.hostname.endsWith('.local') ||
window.location.hostname.startsWith('192.168.') ||
window.location.hostname.startsWith('10.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(window.location.hostname))
) {
return supabaseUrl.replace('127.0.0.1', window.location.hostname);
}

return supabaseUrl;
};

export const supabaseUrl = getSupabaseUrl();

export const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY ?? '';
if (!supabaseAnonKey) {
throw new Error('VITE_SUPABASE_ANON_KEY is not set');
}

const openSecretApiUrl = import.meta.env.VITE_OPEN_SECRET_API_URL ?? '';
if (!openSecretApiUrl) {
throw new Error('VITE_OPEN_SECRET_API_URL is not set');
}

const openSecretClientId = import.meta.env.VITE_OPEN_SECRET_CLIENT_ID ?? '';
if (!openSecretClientId) {
throw new Error('VITE_OPEN_SECRET_CLIENT_ID is not set');
}

// console.debug(message, undefined) prints a trailing "undefined". Most calls
// pass no meta, so the second argument is omitted unless meta is provided.
const consoleLogger = {
debug: (message: string, meta?: unknown) =>
meta === undefined ? console.debug(message) : console.debug(message, meta),
Comment thread
jbojcic1 marked this conversation as resolved.
info: (message: string, meta?: unknown) =>
meta === undefined ? console.info(message) : console.info(message, meta),
warn: (message: string, meta?: unknown) =>
meta === undefined ? console.warn(message) : console.warn(message, meta),
error: (message: string, meta?: unknown) =>
meta === undefined ? console.error(message) : console.error(message, meta),
};

export const sdk = AgicashSdk.create({
db: {
url: supabaseUrl,
anonKey: supabaseAnonKey,
},
auth: {
apiUrl: openSecretApiUrl,
clientId: openSecretClientId,
storage: browserStorage,
// e2e bridge: the Playwright fixture arms window.getMockPassword; in
// production it's absent, so this resolves null and the SDK generates.
generateGuestPassword: async () =>
(await window.getMockPassword?.()) ?? null,
},
spark: {
breezApiKey,
network: 'MAINNET',
},
lightningAddressDomain: window.location.host,
logger: consoleLogger,
});

if (import.meta.hot) {
// A hot reload of this module constructs a second SDK; dispose the old one
// so its expiry timer doesn't leak. Returning the promise makes Vite await
Comment thread
jbojcic1 marked this conversation as resolved.
// the teardown before evaluating the replacement module.
import.meta.hot.dispose(() => sdk.dispose());
}
4 changes: 2 additions & 2 deletions apps/web-wallet/app/features/signup/verify-email.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { verifyEmail as osVerifyEmail } from '@agicash/opensecret';
import type { FullUser } from '@agicash/wallet-sdk';
import { shouldVerifyEmail } from '@agicash/wallet-sdk';
import { useState } from 'react';
import { createContext, redirect } from 'react-router';
import { sdk } from '~/features/shared/sdk.client';
import { useToast } from '~/hooks/use-toast';
import type { Route } from '../../routes/+types/_protected.verify-email.($code)';
import { invalidateAuthQueries } from '../user/auth';
Expand Down Expand Up @@ -37,7 +37,7 @@ export const verifyEmail = async (
code: string,
): Promise<{ verified: true } | { verified: false; error: Error }> => {
try {
await osVerifyEmail(code);
await sdk.auth.verifyEmail(code);
await invalidateAuthQueries();
Comment thread
jbojcic1 marked this conversation as resolved.
return { verified: true };
} catch (e) {
Expand Down
Loading
Loading