Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
fab4053
feat: initial spark tokenization implementation
WendellMorTamayo Nov 10, 2025
1ddc99a
feat: add issuer-sdk dependency and enhance Spark wallet functionality
WendellMorTamayo Nov 13, 2025
484c1dc
feat: refactor Spark wallet functionality and add new token managemen…
WendellMorTamayo Nov 13, 2025
46f8053
feat: implement multi-chain wallet support
WendellMorTamayo Nov 14, 2025
9ea13b0
refactor: clean up wallet retrieval options and improve documentation
WendellMorTamayo Nov 17, 2025
66f2242
Merge branch 'main' of github.com:MeshJS/web3-sdk into feat/tokenization
WendellMorTamayo Nov 17, 2025
c524364
refactor: wallet chain checks and update tokenization parameter naming
WendellMorTamayo Nov 17, 2025
f8bba3f
feat: add Cardano token operations and metadata handling; enhance Spa…
WendellMorTamayo Nov 17, 2025
3f982a9
refactor: rename wallet handlers for clarity in WalletDeveloperContro…
WendellMorTamayo Nov 17, 2025
71ad21c
refactor: simplify wallet retrieval by removing walletId parameter fr…
WendellMorTamayo Nov 18, 2025
1866ce1
feat: spark dev controlled wallet update sdk
WendellMorTamayo Nov 21, 2025
ccfdb01
feat: dev controlled spark wallet update
WendellMorTamayo Nov 24, 2025
049537c
feat: add transaction logging for spark token operations and type imp…
WendellMorTamayo Nov 25, 2025
72fc321
feedback and improve spark tokens
jinglescode Nov 26, 2025
a2e83a8
feat: add enableTokenization option for wallet-token creation flow
WendellMorTamayo Nov 26, 2025
7667c05
fix: spark transfer
WendellMorTamayo Nov 26, 2025
e5b0649
feat(tokenization): init cardano tokenization and cleanup
WendellMorTamayo Nov 28, 2025
c754b77
refactor: remove cardano tokenization for dev wallet
WendellMorTamayo Nov 28, 2025
fe74786
refactor: remove unnecessary files and codes
WendellMorTamayo Nov 28, 2025
54982bd
fix: sponsorship is for cardano only
WendellMorTamayo Nov 28, 2025
565e9db
fix: update docs to be accurate
WendellMorTamayo Nov 28, 2025
1f04cee
feat: update inline docs in sdk
WendellMorTamayo Nov 28, 2025
b5c208a
feat: clean up spark tokenization and dev wallet
WendellMorTamayo Nov 28, 2025
26eee75
feat(tokenization): update transaction logging and wallet initialization
WendellMorTamayo Nov 28, 2025
0d817d6
Merge branch 'main' of github.com:MeshJS/web3-sdk into feat/tokenization
WendellMorTamayo Nov 28, 2025
b0b9c28
refactor(sdk): bundle createWallet and createToken for seamless token…
WendellMorTamayo Nov 30, 2025
74c7e6a
update from main branch
jinglescode Jan 6, 2026
2c95f7a
fix build errors
jinglescode Jan 6, 2026
c1f7978
refactor developer controlled wallet
jinglescode Jan 8, 2026
6deb084
refactor spark token
jinglescode Jan 8, 2026
07fa8a3
added a lot of tests
jinglescode Jan 9, 2026
5bf12ae
feat(wallet): add pagination support for getProjectWallets
jinglescode Jan 23, 2026
b65d668
fix(deps): pin api-contracts version to ^0.0.1
jinglescode Jan 23, 2026
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
75 changes: 75 additions & 0 deletions examples/developer-controlled-wallet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Web3Sdk } from "@utxos/sdk";

/**
* Example: Developer-Controlled Wallet
*
* This example demonstrates wallet management with multi-chain support (Spark and Cardano).
* Use sdk.wallet.* for wallet operations.
* Use sdk.tokenization.spark.* for token operations (see tokenization example).
*/

async function main() {
// Initialize SDK
const sdk = new Web3Sdk({
projectId: "your-project-id",
apiKey: "your-api-key",
network: "testnet", // or "mainnet"
appUrl: "https://your-app.com",
privateKey: "your-private-key", // Required for developer-controlled wallets
});

// === CREATE WALLET ===

console.log("Creating developer-controlled wallet...");

// Create wallet with both Spark and Cardano chains (shared mnemonic)
const { info, sparkIssuerWallet, cardanoWallet } = await sdk.wallet.createWallet({
tags: ["treasury"],
});

console.log("Wallet created:", info.id);

// === LIST WALLETS ===

console.log("\nListing project wallets (paginated)...");
const { data: wallets, pagination } = await sdk.wallet.getProjectWallets();
console.log(`Found ${wallets.length} wallets on page ${pagination.page} of ${pagination.totalPages}`);
console.log(`Total wallets: ${pagination.totalCount}`);

// Or get all wallets at once
const allWallets = await sdk.wallet.getAllProjectWallets();
console.log(`All wallets: ${allWallets.length}`);

// === GET WALLET BY CHAIN ===

console.log("\nGetting wallet for specific chain...");

// Get Cardano wallet
const { cardanoWallet: cardano } = await sdk.wallet.getWallet(info.id, "cardano");
const addresses = cardano!.getAddresses();
console.log("Cardano base address:", addresses.baseAddressBech32);

// Get Spark wallet info
const sparkWalletInfo = await sdk.wallet.sparkIssuer.getWallet(info.id);
console.log("Spark wallet public key:", sparkWalletInfo.publicKey);

// === LOAD EXISTING WALLET ===

console.log("\nLoading existing wallet...");
const { info: existingInfo, sparkWallet, cardanoWallet: existingCardano } =
await sdk.wallet.initWallet("existing-wallet-id");

console.log("Loaded wallet:", existingInfo.id);

// === LIST BY TAG ===

console.log("\nListing wallets by tag...");
const sparkWallets = await sdk.wallet.sparkIssuer.getByTag("treasury");
const cardanoWallets = await sdk.wallet.cardano.getWalletsByTag("treasury");

console.log(`Found ${sparkWallets.length} Spark wallets with 'treasury' tag`);
console.log(`Found ${cardanoWallets.length} Cardano wallets with 'treasury' tag`);
}

// Run example
main().catch(console.error);
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@utxos/sdk",
"version": "0.0.78",
"version": "0.1.0",
"description": "UTXOS SDK - Web3 infrastructure platform for UTXO blockchains",
"main": "./dist/index.cjs",
"browser": "./dist/index.js",
Expand Down Expand Up @@ -28,7 +28,7 @@
"scripts": {
"build:sdk": "tsup src/index.ts --format esm,cjs --dts",
"dev": "tsup src/index.ts --format esm,cjs --watch --dts",
"test": "jest"
"test": "npx jest"
},
"devDependencies": {
"@types/base32-encoding": "^1.0.2",
Expand All @@ -41,6 +41,8 @@
"typescript": "^5.3.3"
},
"dependencies": {
"@utxos/api-contracts": "^0.0.1",
"@buildonspark/issuer-sdk": "^0.1.5",
"@buildonspark/spark-sdk": "0.5.0",
"@meshsdk/bitcoin": "1.9.0-beta.89",
"@meshsdk/common": "1.9.0-beta.89",
Expand Down
287 changes: 287 additions & 0 deletions src/functions/client/derive-wallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import { crypto } from "../crypto";
import { encryptWithCipher } from "../crypto";
import { spiltKeyIntoShards } from "../key-shard";

// Mock external wallet SDKs
jest.mock("@meshsdk/wallet", () => ({
MeshWallet: jest.fn().mockImplementation(() => ({
init: jest.fn().mockResolvedValue(undefined),
getUsedAddresses: jest.fn().mockResolvedValue(["addr_test1..."]),
})),
}));

jest.mock("@meshsdk/bitcoin", () => ({
EmbeddedWallet: jest.fn().mockImplementation(() => ({
getAddress: jest.fn().mockResolvedValue("bc1q..."),
})),
}));

jest.mock("@buildonspark/spark-sdk", () => ({
SparkWallet: {
initialize: jest.fn().mockResolvedValue({
wallet: {
getAddress: jest.fn().mockResolvedValue("spark1..."),
},
}),
},
}));

// Import after mocks
import { clientDeriveWallet } from "./derive-wallet";
import { MeshWallet } from "@meshsdk/wallet";
import { EmbeddedWallet } from "@meshsdk/bitcoin";
import { SparkWallet } from "@buildonspark/spark-sdk";

async function deriveKeyFromPassword(password: string): Promise<CryptoKey> {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
"PBKDF2",
false,
["deriveKey"],
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: encoder.encode("static-salt-for-test"),
iterations: 100000,
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"],
);
}

describe("clientDeriveWallet", () => {
const testMnemonic =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";

beforeEach(() => {
jest.clearAllMocks();
});

it("derives wallet from encrypted device shard and custodial shard", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");

// Encrypt the device shard (shard 1)
const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: deviceKey,
});

// Custodial shard is shard 2 (auth shard)
const custodialShard = shards[1]!;

const result = await clientDeriveWallet(
encryptedDeviceShard,
deviceKey,
custodialShard,
0, // testnet
);

expect(result).toHaveProperty("bitcoinWallet");
expect(result).toHaveProperty("cardanoWallet");
expect(result).toHaveProperty("sparkWallet");
expect(result).toHaveProperty("key");
});

it("returns the reconstructed mnemonic as key", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");

const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: deviceKey,
});

const result = await clientDeriveWallet(
encryptedDeviceShard,
deviceKey,
shards[1]!,
0,
);

expect(result.key).toBe(testMnemonic);
});

it("creates wallets with testnet configuration when networkId is 0", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");

const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: deviceKey,
});

await clientDeriveWallet(encryptedDeviceShard, deviceKey, shards[1]!, 0);

expect(EmbeddedWallet).toHaveBeenCalledWith(
expect.objectContaining({ network: "Testnet" }),
);
expect(MeshWallet).toHaveBeenCalledWith(
expect.objectContaining({ networkId: 0 }),
);
expect(SparkWallet.initialize).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({ network: "REGTEST" }),
}),
);
});

it("creates wallets with mainnet configuration when networkId is 1", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");

const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: deviceKey,
});

await clientDeriveWallet(encryptedDeviceShard, deviceKey, shards[1]!, 1);

expect(EmbeddedWallet).toHaveBeenCalledWith(
expect.objectContaining({ network: "Mainnet" }),
);
expect(MeshWallet).toHaveBeenCalledWith(
expect.objectContaining({ networkId: 1 }),
);
expect(SparkWallet.initialize).toHaveBeenCalledWith(
expect.objectContaining({
options: expect.objectContaining({ network: "MAINNET" }),
}),
);
});

it("passes bitcoinProvider when provided", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");
const mockProvider = { getUtxos: jest.fn() };

const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: deviceKey,
});

await clientDeriveWallet(
encryptedDeviceShard,
deviceKey,
shards[1]!,
0,
mockProvider as any,
);

expect(EmbeddedWallet).toHaveBeenCalledWith(
expect.objectContaining({ provider: mockProvider }),
);
});

it("fails with wrong decryption key", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const correctKey = await deriveKeyFromPassword("correct-password");
const wrongKey = await deriveKeyFromPassword("wrong-password");

const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: correctKey,
});

await expect(
clientDeriveWallet(encryptedDeviceShard, wrongKey, shards[1]!, 0),
).rejects.toThrow();
});

it("fails with corrupted encrypted shard", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");

const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: deviceKey,
});

// Corrupt the encrypted data
const parsed = JSON.parse(encryptedDeviceShard);
parsed.ciphertext = "corrupted" + parsed.ciphertext;

await expect(
clientDeriveWallet(JSON.stringify(parsed), deviceKey, shards[1]!, 0),
).rejects.toThrow();
});

it("fails with invalid JSON in encrypted shard", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");

await expect(
clientDeriveWallet("not-valid-json", deviceKey, shards[1]!, 0),
).rejects.toThrow();
});

it("works with shards from different positions", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");

// Use shard 2 as device shard and shard 3 as custodial
const encryptedDeviceShard = await encryptWithCipher({
data: shards[1]!,
key: deviceKey,
});

const result = await clientDeriveWallet(
encryptedDeviceShard,
deviceKey,
shards[2]!,
0,
);

expect(result.key).toBe(testMnemonic);
});

it("initializes cardano wallet", async () => {
const shards = await spiltKeyIntoShards(testMnemonic);
const deviceKey = await deriveKeyFromPassword("device-password");

const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: deviceKey,
});

const result = await clientDeriveWallet(
encryptedDeviceShard,
deviceKey,
shards[1]!,
0,
);

expect(result.cardanoWallet.init).toHaveBeenCalled();
});
});

describe("clientDeriveWallet with 24-word mnemonic", () => {
const mnemonic24 =
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art";

it("derives wallet from 24-word mnemonic shards", async () => {
const shards = await spiltKeyIntoShards(mnemonic24);
const deviceKey = await deriveKeyFromPassword("device-password");

const encryptedDeviceShard = await encryptWithCipher({
data: shards[0]!,
key: deviceKey,
});

const result = await clientDeriveWallet(
encryptedDeviceShard,
deviceKey,
shards[1]!,
0,
);

expect(result.key).toBe(mnemonic24);
expect(result.key.split(" ").length).toBe(24);
});
});
Loading