Wallets
How to add a new wallet
1. Update prisma.schema
- add enum value to
WalletName
enum - run
npx prisma migrate dev --create-only
2. Update migration file
- append
COMMIT
to theALTER TYPE
statement in the migration - insert new row into
ẀalletTemplate
- run
npx prisma migrate dev
Example migration:
ALTER TYPE "WalletName" ADD VALUE 'PHOENIX'; COMMIT;
INSERT INTO "WalletTemplate" (name, "sendProtocols", "recvProtocols")
VALUES (
'PHOENIX',
ARRAY[]::"WalletSendProtocolName"[],
ARRAY['BOLT12']::"WalletRecvProtocolName"[]
);
3. Customize how the wallet looks on the client via wallets/lib/wallets.json
Example:
{
// must be same name as wallet template
"name": "PHOENIX",
// name to show in client
"displayName": "Phoenix",
// image to show in client
// (dark mode will use /path/to/image-dark.png)
"image": "/path/to/image.png",
// url (planned) to show in client
"url": "https://phoenix.acinq.co/"
}
If the wallet supports a lightning address and the domain is different than the url, you can pass an object to url
. Here is Zeus as an example:
{
"name": "ZEUS",
"displayName": "Zeus",
"image": "/wallets/zeus.svg",
"url": {
"wallet": "https://zeusln.com/",
// different domain for lightning address
"lud16Domain": "zeuspay.com"
}
},
That's it!
How to add a new protocol
1. Update db schema
1.1 Update prisma.schema
- add enum value to
WalletProtocolName
enum - add enum value to
WalletRecvProtocolName
orWalletSendProtocolName
- add table to store protocol config
- run
npx prisma migrate dev --create-only
- for send protocols, it is important that the names for encrypted columns end with
vaultId
1.2 Update migration file
- add
COMMIT
after statements to add enum values - add required triggers:
wallet_to_jsonb
and if send protocol, alsowallet_clear_vault
- run
npx prisma migrate dev
Example
commit 0834650e84e3c0ba86f881f0f3643e87b26108e7
Author: ekzyis <ek@stacker.news>
Date: Tue Sep 23 07:24:37 2025 +0200
DB schema for Spark
diff --git a/prisma/migrations/20250923052230_spark/migration.sql b/prisma/migrations/20250923052230_spark/migration.sql
new file mode 100644
index 00000000..04ff1847
--- /dev/null
+++ b/prisma/migrations/20250923052230_spark/migration.sql
@@ -0,0 +1,64 @@
+-- AlterEnum
+ALTER TYPE "WalletName" ADD VALUE 'SPARK'; COMMIT;
+
+-- AlterEnum
+ALTER TYPE "WalletProtocolName" ADD VALUE 'SPARK'; COMMIT;
+
+-- AlterEnum
+ALTER TYPE "WalletRecvProtocolName" ADD VALUE 'SPARK'; COMMIT;
+
+-- AlterEnum
+ALTER TYPE "WalletSendProtocolName" ADD VALUE 'SPARK'; COMMIT;
+
+INSERT INTO "WalletTemplate" ("name", "sendProtocols", "recvProtocols")
+VALUES ('SPARK', '{SPARK}', '{SPARK}');
+
+-- CreateTable
+CREATE TABLE "WalletSendSpark" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "protocolId" INTEGER NOT NULL,
+ "mnemonicVaultId" INTEGER NOT NULL,
+
+ CONSTRAINT "WalletSendSpark_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "WalletRecvSpark" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "protocolId" INTEGER NOT NULL,
+ "address" TEXT NOT NULL,
+
+ CONSTRAINT "WalletRecvSpark_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "WalletSendSpark_protocolId_key" ON "WalletSendSpark"("protocolId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "WalletSendSpark_mnemonicVaultId_key" ON "WalletSendSpark"("mnemonicVaultId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "WalletRecvSpark_protocolId_key" ON "WalletRecvSpark"("protocolId");
+
+-- AddForeignKey
+ALTER TABLE "WalletSendSpark" ADD CONSTRAINT "WalletSendSpark_protocolId_fkey" FOREIGN KEY ("protocolId") REFERENCES "WalletProtocol"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "WalletSendSpark" ADD CONSTRAINT "WalletSendSpark_mnemonicVaultId_fkey" FOREIGN KEY ("mnemonicVaultId") REFERENCES "Vault"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "WalletRecvSpark" ADD CONSTRAINT "WalletRecvSpark_protocolId_fkey" FOREIGN KEY ("protocolId") REFERENCES "WalletProtocol"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+CREATE TRIGGER wallet_to_jsonb
+ AFTER INSERT OR UPDATE ON "WalletSendSpark"
+ FOR EACH ROW
+ EXECUTE PROCEDURE wallet_to_jsonb();
+
+CREATE TRIGGER wallet_clear_vault
+ AFTER DELETE ON "WalletSendSpark"
+ FOR EACH ROW
+ EXECUTE PROCEDURE wallet_clear_vault();
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 4f3cb9e2..c25c0fc8 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -221,6 +221,7 @@ model Vault {
walletSendLNCRemoteKey WalletSendLNC? @relation("lncRemoteKey")
walletSendLNCServerHost WalletSendLNC? @relation("lncServerHost")
walletSendCLNRestRune WalletSendCLNRest? @relation("clnRune")
+ walletSendSparkMnemonic WalletSendSpark? @relation("sparkMnemonic")
}
model WalletLog {
@@ -1214,6 +1215,7 @@ enum WalletProtocolName {
CLN_REST
LND_GRPC
CLINK
+ SPARK
}
enum WalletSendProtocolName {
@@ -1224,6 +1226,7 @@ enum WalletSendProtocolName {
WEBLN
LNC
CLN_REST
+ SPARK
}
enum WalletRecvProtocolName {
@@ -1235,6 +1238,7 @@ enum WalletRecvProtocolName {
CLN_REST
LND_GRPC
CLINK
+ SPARK
}
enum WalletProtocolStatus {
@@ -1270,6 +1274,7 @@ enum WalletName {
LN_ADDR
CASH_APP
BLITZ
+ SPARK
}
model WalletTemplate {
@@ -1327,6 +1332,7 @@ model WalletProtocol {
walletSendWebLN WalletSendWebLN?
walletSendLNC WalletSendLNC?
walletSendCLNRest WalletSendCLNRest?
+ walletSendSpark WalletSendSpark?
walletRecvNWC WalletRecvNWC?
walletRecvLNbits WalletRecvLNbits?
@@ -1336,6 +1342,7 @@ model WalletProtocol {
walletRecvCLNRest WalletRecvCLNRest?
walletRecvLNDGRPC WalletRecvLNDGRPC?
walletRecvClink WalletRecvClink?
+ walletRecvSpark WalletRecvSpark?
@@unique(name: "WalletProtocol_walletId_send_name_key", [walletId, send, name])
@@index([walletId])
@@ -1420,6 +1427,16 @@ model WalletSendCLNRest {
rune Vault? @relation("clnRune", fields: [runeVaultId], references: [id])
}
+model WalletSendSpark {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
+ protocolId Int @unique
+ protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
+ mnemonicVaultId Int @unique
+ mnemonic Vault? @relation("sparkMnemonic", fields: [mnemonicVaultId], references: [id])
+}
+
model WalletRecvNWC {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
@@ -1498,3 +1515,12 @@ model WalletRecvClink {
protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
noffer String
}
+
+model WalletRecvSpark {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map("created_at")
+ updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
+ protocolId Int @unique
+ protocol WalletProtocol @relation(fields: [protocolId], references: [id], onDelete: Cascade)
+ address String
+}
2. Update JS code
2.1 Add protocol lib file
- add file to wallets/lib/protocols (see JSDoc for details)
- import in index.js file and add to default export
2.2 Add protocol method file
- if protocol to receive payments: Add file to wallets/server/protocols (see JSDoc for details)
- if protocol to send payments: Add file to wallets/client/protocols (see JSDoc for details)
- import in index.js file and add to default export
Example
commit 4fd125fcad91dc5bee31f076785c0b97337311a8
Author: ekzyis <ek@stacker.news>
Date: Tue Sep 23 07:41:47 2025 +0200
JS code for Spark
diff --git a/wallets/client/protocols/index.js b/wallets/client/protocols/index.js
index c25805f7..3d6b00f6 100644
--- a/wallets/client/protocols/index.js
+++ b/wallets/client/protocols/index.js
@@ -5,6 +5,7 @@ import * as blink from './blink'
import * as webln from './webln'
import * as lnc from './lnc'
import * as clnRest from './clnRest'
+import * as spark from './spark'
export * from './util'
@@ -54,5 +55,6 @@ export default [
blink,
webln,
lnc,
- clnRest
+ clnRest,
+ spark
]
diff --git a/wallets/client/protocols/spark.js b/wallets/client/protocols/spark.js
new file mode 100644
index 00000000..ef3209e6
--- /dev/null
+++ b/wallets/client/protocols/spark.js
@@ -0,0 +1,7 @@
+export const name = 'SPARK'
+
+export async function sendPayment (bolt11, { mnemonic }, { signal }) {
+ // TODO: implement
+}
+
+export async function testSendPayment (config, { signal }) {}
diff --git a/wallets/lib/protocols/index.js b/wallets/lib/protocols/index.js
index 5a944124..633659c5 100644
--- a/wallets/lib/protocols/index.js
+++ b/wallets/lib/protocols/index.js
@@ -8,6 +8,7 @@ import phoenixdSuite from './phoenixd'
import blinkSuite from './blink'
import webln from './webln'
import clink from './clink'
+import sparkSuite from './spark'
/**
* Protocol names as used in the database
@@ -47,5 +48,6 @@ export default [
...lnbitsSuite,
...blinkSuite,
webln,
- clink
+ clink,
+ ...sparkSuite
]
diff --git a/wallets/lib/protocols/spark.js b/wallets/lib/protocols/spark.js
new file mode 100644
index 00000000..b1b05608
--- /dev/null
+++ b/wallets/lib/protocols/spark.js
@@ -0,0 +1,41 @@
+import { bip39Validator, externalLightningAddressValidator } from '@/wallets/lib/validate'
+
+// Spark
+// https://github.com/breez/spark-sdk
+// https://sdk-doc-spark.breez.technology/
+
+export default [
+ {
+ name: 'SPARK',
+ send: true,
+ displayName: 'Spark',
+ fields: [
+ {
+ name: 'mnemonic',
+ label: 'mnemonic',
+ type: 'password',
+ required: true,
+ validate: bip39Validator(),
+ encrypt: true
+ }
+ ],
+ relationName: 'walletSendSpark'
+ },
+ {
+ name: 'SPARK',
+ send: false,
+ displayName: 'Spark',
+ fields: [
+ {
+ name: 'address',
+ label: 'address',
+ type: 'text',
+ required: true,
+ validate: externalLightningAddressValidator
+ }
+ ],
+ relationName: 'walletRecvSpark'
+ }
+]
diff --git a/wallets/lib/wallets.json b/wallets/lib/wallets.json
index 1975a9d2..98fe5bf8 100644
--- a/wallets/lib/wallets.json
+++ b/wallets/lib/wallets.json
@@ -168,5 +168,9 @@
"displayName": "Blitz Wallet",
"image": "/wallets/blitz.png",
"url": "https://blitz-wallet.com/"
+ },
+ {
+ "name": "SPARK",
+ "displayName": "Spark"
}
]
diff --git a/wallets/server/protocols/index.js b/wallets/server/protocols/index.js
index 6bf8ca04..6151e217 100644
--- a/wallets/server/protocols/index.js
+++ b/wallets/server/protocols/index.js
@@ -6,6 +6,7 @@ import * as phoenixd from './phoenixd'
import * as blink from './blink'
import * as lndGrpc from './lndGrpc'
import * as clink from './clink'
+import * as spark from './spark'
export * from './util'
@@ -58,5 +59,6 @@ export default [
phoenixd,
blink,
lndGrpc,
- clink
+ clink,
+ spark
]
diff --git a/wallets/server/protocols/spark.js b/wallets/server/protocols/spark.js
new file mode 100644
index 00000000..abc610ac
--- /dev/null
+++ b/wallets/server/protocols/spark.js
@@ -0,0 +1,16 @@
+export const name = 'SPARK'
+
+export async function createInvoice (
+ { msats, description, descriptionHash, expiry },
+ { address },
+ { signal }
+) {
+ // TODO: implement
+}
+
+export async function testCreateInvoice ({ address }, { signal }) {
+ return await createInvoice(
+ { msats: 1000, description: 'SN test invoice', expiry: 1 },
+ { address },
+ { signal })
+}
3. Update GraphQL code
- add GraphQL type
- add GraphQL type to
WalletProtocolConfig
union - add GraphQL type to
WalletProtocolFields
fragment via spread operator (...) - add GraphQL mutation to upsert and test protocol
- resolve GraphQL type in
mapWalletResolveTypes
function
Example
commit 72c9d3a46928775d66ac93ed1e66294f435bbcb7
Author: ekzyis <ek@stacker.news>
Date: Tue Sep 23 07:55:17 2025 +0200
GraphQL code for Spark
diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js
index 6284b821..7420ec15 100644
--- a/api/typeDefs/wallet.js
+++ b/api/typeDefs/wallet.js
@@ -108,6 +108,16 @@ const typeDefs = gql`
${shared}
): WalletSendWebLN!
+ upsertWalletSendSpark(
+ ${shared},
+ mnemonic: VaultEntryInput!
+ ): WalletSendSpark!
+
+ upsertWalletRecvSpark(
+ ${shared},
+ address: String!
+ ): WalletRecvSpark!
+
upsertWalletRecvClink(
${shared},
noffer: String!
@@ -153,6 +163,10 @@ const typeDefs = gql`
noffer: String!
): Boolean!
+ testWalletRecvSpark(
+ address: String!
+ ): Boolean!
+
# delete
deleteWallet(id: ID!): Boolean
@@ -228,6 +242,7 @@ const typeDefs = gql`
| WalletSendWebLN
| WalletSendLNC
| WalletSendCLNRest
+ | WalletSendSpark
| WalletRecvNWC
| WalletRecvLNbits
| WalletRecvPhoenixd
@@ -236,6 +251,7 @@ const typeDefs = gql`
| WalletRecvCLNRest
| WalletRecvLNDGRPC
| WalletRecvClink
+ | WalletRecvSpark
type WalletSettings {
receiveCreditsBelowSats: Int!
@@ -296,6 +312,11 @@ const typeDefs = gql`
rune: VaultEntry!
}
+ type WalletSendSpark {
+ id: ID!
+ mnemonic: VaultEntry!
+ }
+
type WalletRecvNWC {
id: ID!
url: String!
@@ -343,6 +364,11 @@ const typeDefs = gql`
noffer: String!
}
+ type WalletRecvSpark {
+ id: ID!
+ address: String!
+ }
+
input AutowithdrawSettings {
autoWithdrawThreshold: Int!
autoWithdrawMaxFeePercent: Float!
diff --git a/wallets/client/fragments/protocol.js b/wallets/client/fragments/protocol.js
index 8b132e82..38586def 100644
--- a/wallets/client/fragments/protocol.js
+++ b/wallets/client/fragments/protocol.js
@@ -249,6 +249,34 @@ export const UPSERT_WALLET_RECEIVE_CLINK = gql`
}
`
+export const UPSERT_WALLET_SEND_SPARK = gql`
+ mutation upsertWalletSendSpark(
+ ${shared.variables},
+ $mnemonic: VaultEntryInput!
+ ) {
+ upsertWalletSendSpark(
+ ${shared.arguments},
+ mnemonic: $mnemonic
+ ) {
+ id
+ }
+ }
+`
+
+export const UPSERT_WALLET_RECEIVE_SPARK = gql`
+ mutation upsertWalletRecvSpark(
+ ${shared.variables},
+ $address: String!
+ ) {
+ upsertWalletRecvSpark(
+ ${shared.arguments},
+ address: $address
+ ) {
+ id
+ }
+ }
+`
+
// tests
export const TEST_WALLET_RECEIVE_NWC = gql`
@@ -298,3 +326,9 @@ export const TEST_WALLET_RECEIVE_CLINK = gql`
testWalletRecvClink(noffer: $noffer)
}
`
+
+export const TEST_WALLET_RECEIVE_SPARK = gql`
+ mutation testWalletRecvSpark($address: String!) {
+ testWalletRecvSpark(address: $address)
+ }
+`
diff --git a/wallets/client/fragments/wallet.js b/wallets/client/fragments/wallet.js
index 6d8676cc..b646c890 100644
--- a/wallets/client/fragments/wallet.js
+++ b/wallets/client/fragments/wallet.js
@@ -78,6 +78,12 @@ const WALLET_PROTOCOL_FIELDS = gql`
...VaultEntryFields
}
}
+ ... on WalletSendSpark {
+ id
+ encryptedMnemonic: mnemonic {
+ ...VaultEntryFields
+ }
+ }
... on WalletRecvNWC {
id
url
@@ -117,6 +123,10 @@ const WALLET_PROTOCOL_FIELDS = gql`
id
noffer
}
+ ... on WalletRecvSpark {
+ id
+ address
+ }
}
}
`
diff --git a/wallets/client/hooks/query.js b/wallets/client/hooks/query.js
index 51cf44b0..37169a69 100644
--- a/wallets/client/hooks/query.js
+++ b/wallets/client/hooks/query.js
@@ -13,6 +13,7 @@ import {
UPSERT_WALLET_RECEIVE_NWC,
UPSERT_WALLET_RECEIVE_PHOENIXD,
UPSERT_WALLET_RECEIVE_CLINK,
+ UPSERT_WALLET_RECEIVE_SPARK,
UPSERT_WALLET_SEND_BLINK,
UPSERT_WALLET_SEND_LNBITS,
UPSERT_WALLET_SEND_LNC,
@@ -20,6 +21,7 @@ import {
UPSERT_WALLET_SEND_PHOENIXD,
UPSERT_WALLET_SEND_WEBLN,
UPSERT_WALLET_SEND_CLN_REST,
+ UPSERT_WALLET_SEND_SPARK,
WALLETS,
UPDATE_WALLET_ENCRYPTION,
RESET_WALLETS,
@@ -34,6 +36,7 @@ import {
TEST_WALLET_RECEIVE_CLN_REST,
TEST_WALLET_RECEIVE_LND_GRPC,
TEST_WALLET_RECEIVE_CLINK,
+ TEST_WALLET_RECEIVE_SPARK,
DELETE_WALLET
} from '@/wallets/client/fragments'
import { gql, useApolloClient, useMutation, useQuery } from '@apollo/client'
@@ -320,6 +323,8 @@ function protocolUpsertMutation (protocol) {
return protocol.send ? UPSERT_WALLET_SEND_WEBLN : NOOP_MUTATION
case 'CLINK':
return protocol.send ? NOOP_MUTATION : UPSERT_WALLET_RECEIVE_CLINK
+ case 'SPARK':
+ return protocol.send ? UPSERT_WALLET_SEND_SPARK : UPSERT_WALLET_RECEIVE_SPARK
default:
return NOOP_MUTATION
}
@@ -345,6 +350,8 @@ function protocolTestMutation (protocol) {
return TEST_WALLET_RECEIVE_LND_GRPC
case 'CLINK':
return TEST_WALLET_RECEIVE_CLINK
+ case 'SPARK':
+ return TEST_WALLET_RECEIVE_SPARK
default:
return NOOP_MUTATION
}
diff --git a/wallets/server/resolvers/util.js b/wallets/server/resolvers/util.js
index e11ee3e1..6d3741bc 100644
--- a/wallets/server/resolvers/util.js
+++ b/wallets/server/resolvers/util.js
@@ -21,6 +21,8 @@ export function mapWalletResolveTypes (wallet) {
return 'WalletRecvLNDGRPC'
case 'CLINK':
return 'WalletRecvClink'
+ case 'SPARK':
+ return send ? 'WalletSendSpark' : 'WalletRecvSpark'
default:
return null
}