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
19 changes: 6 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ pnpm monorepo publishing three deeplink packages used across Rozo apps for QR co

## Packages

| Package | NPM name | Purpose |
|---|---|---|
| `packages/core` | `@rozoai/deeplink-core` | Core deeplink URL builder, QR spec |
| `packages/react` | `@rozoai/deeplink-react` | React hooks + components |
| `packages/react-native` | `@rozoai/deeplink-react-native` | React Native hooks + components |
| `apps/` | demo apps | Integration examples |
| Package | NPM name | Purpose |
| ----------------------- | ------------------------------- | ---------------------------------- |
| `packages/core` | `@rozoai/deeplink-core` | Core deeplink URL builder, QR spec |
| `packages/react` | `@rozoai/deeplink-react` | React hooks + components |
| `packages/react-native` | `@rozoai/deeplink-react-native` | React Native hooks + components |
| `apps/` | demo apps | Integration examples |

## Development Commands

Expand Down Expand Up @@ -49,10 +49,3 @@ cd packages/core && pnpm release
- `QRSpec.md` documents the QR code URL format spec
- Used by: `rozo-invoice`, `rozo-privy`, `rozo-wallets-express`, `intent-pay`
- Published as `@rozoai/deeplink-*` to npm

## Helper Scripts

```bash
chp # Project overview
chs find-code "pattern" # Fast search
```
37 changes: 23 additions & 14 deletions QRSpec.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ This document provides practical QR code string examples for supported deeplink
- [6. Solana Pay – interactive transaction link (Base64-encoded URL)](#6-solana-pay--interactive-transaction-link-base64-encoded-url)
- [7. Solana Pay – minimal (plain address, no params)](#7-solana-pay--minimal-plain-address-no-params)
- [Stellar](#stellar)
- [1. Stellar Address (plain)](#1-stellar-address-plain)
- [2. SEP-7 Pay Request (XLM)](#2-sep-7-pay-request-xlm)
- [3. SEP-7 Pay Request (with asset)](#3-sep-7-pay-request-with-asset)
- [4. SEP-7 Pay with memo and callback](#4-sep-7-pay-with-memo-and-callback)
- [5. SEP-7 Pay with message and origin domain](#5-sep-7-pay-with-message-and-origin-domain)
- [6. SEP-7 Transaction (XDR)](#6-sep-7-transaction-xdr)
- [7. SEP-7 Pay with extra custom params](#7-sep-7-pay-with-extra-custom-params)
- [1. Stellar Address (plain G address)](#1-stellar-address-plain-g-address)
- [2. Stellar Contract Address (C address)](#2-stellar-contract-address-c-address)
- [3. SEP-7 Pay Request (XLM)](#3-sep-7-pay-request-xlm)
- [4. SEP-7 Pay Request (with asset)](#4-sep-7-pay-request-with-asset)
- [5. SEP-7 Pay with memo and callback](#5-sep-7-pay-with-memo-and-callback)
- [6. SEP-7 Pay with message and origin domain](#6-sep-7-pay-with-message-and-origin-domain)
- [7. SEP-7 Transaction (XDR)](#7-sep-7-transaction-xdr)
- [8. SEP-7 Pay with extra custom params](#8-sep-7-pay-with-extra-custom-params)
- [Websites](#websites)
- [1. HTTP](#1-http)
- [2. HTTPS (root)](#2-https-root)
Expand Down Expand Up @@ -129,43 +130,51 @@ solana:3n7eT2xzUrWGWwYWnAqymPJrhRuQ9wKziZjqybjEK9ZT

## Stellar

### 1. Stellar Address (plain)
### 1. Stellar Address (plain G address)

```
GC65CUPW2IMTJJY6CII7F3OBPVG4YGASEPBBLM4V3LBKX62P6LA24OFV
```

### 2. SEP-7 Pay Request (XLM)
### 2. Stellar Contract Address (C address)

```
CAHFUV2MVZ3N4VUNQ7RHECTI337SQ2D3QG7TRDSH37ND6M43D6NZANRR
```

> **Note:** C-addresses are Soroban smart contracts, not user accounts. `parseAddress()` returns `{ type: "stellar", address: ... }` **without** `operation: "pay"`. Consumers should not start an account-payment flow for contract addresses.

### 3. SEP-7 Pay Request (XLM)

```
web+stellar:pay?destination=GC65CUPW2IMTJJY6CII7F3OBPVG4YGASEPBBLM4V3LBKX62P6LA24OFV&amount=120.5
```

### 3. SEP-7 Pay Request (with asset)
### 4. SEP-7 Pay Request (with asset)

```
web+stellar:pay?destination=GC65CUPW2IMTJJY6CII7F3OBPVG4YGASEPBBLM4V3LBKX62P6LA24OFV&asset_code=USDC&asset_issuer=GDGU5OAPHNPU5UCLE5RDJHG7PXZFQYWKCFOEXSXNMR6KRQRI5T6XXCD7&amount=100
```

### 4. SEP-7 Pay with memo and callback
### 5. SEP-7 Pay with memo and callback

```
web+stellar:pay?destination=GC65CUPW2IMTJJY6CII7F3OBPVG4YGASEPBBLM4V3LBKX62P6LA24OFV&amount=55.55&memo=Invoice123&memo_type=text&callback=https%3A%2F%2Fshop.com%2Fcb
```

### 5. SEP-7 Pay with message and origin domain
### 6. SEP-7 Pay with message and origin domain

```
web+stellar:pay?destination=GC65CUPW2IMTJJY6CII7F3OBPVG4YGASEPBBLM4V3LBKX62P6LA24OFV&msg=For payment&origin_domain=merchant.com
```

### 6. SEP-7 Transaction (XDR)
### 7. SEP-7 Transaction (XDR)

```
web+stellar:tx?xdr=AAAAAgAAAAAFrgdcAAAAAgAAAAAAAAAAAAAAAwAAAAAAAAAAUQAAAAAAABMtAAAAAAEAAQAAAAEAAAAAAAAAAQAAAABU2lkHD5fTYNbAABehcVQAAAAAA==&replace=0,1
```

### 7. SEP-7 Pay with extra custom params
### 8. SEP-7 Pay with extra custom params

```
web+stellar:pay?destination=GC65CUPW2IMTJJY6CII7F3OBPVG4YGASEPBBLM4V3LBKX62P6LA24OFV&amount=46.1&custom_param=rozo&another_param=test
Expand Down
4 changes: 2 additions & 2 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-slot": "^1.2.3",
"@rozoai/deeplink-core": "1.1.4",
"@rozoai/deeplink-react": "1.1.4",
"@rozoai/deeplink-core": "1.1.7",
"@rozoai/deeplink-react": "1.1.7",
"@uiw/react-json-view": "2.0.0-alpha.33",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
26 changes: 13 additions & 13 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

A universal QR code / deeplink parser that supports multiple blockchain protocols and payment standards.

**Version**: 1.1.4

## Overview

`parseDeeplink` automatically detects and parses QR codes or deeplink strings for:
Expand Down Expand Up @@ -135,8 +133,8 @@ export type DeeplinkData =
interface EthereumParseResult {
type: "ethereum";
operation?: "transfer" | "transaction" | "contract_call" | string;
address?: string; // recipient or spender
token_address?: string; // ERC-20 contract address
address?: string; // recipient or spender
token_address?: string; // ERC-20 contract address
chain_id?: string | number;
amount?: string;
fee?: {
Expand All @@ -158,12 +156,12 @@ interface SolanaParseResult {
operation?: "transfer" | "transaction" | "program_call";
address?: string;
amount?: string;
asset?: { contract?: string }; // SPL token mint address
asset?: { contract?: string }; // SPL token mint address
memo?: string;
origin_domain?: string; // mapped from `label` param
user_message?: string; // mapped from `message` param
callback?: string; // HTTPS link for transaction requests
extra_params?: Record<string, string>; // includes `reference` keys
origin_domain?: string; // mapped from `label` param
user_message?: string; // mapped from `message` param
callback?: string; // HTTPS link for transaction requests
extra_params?: Record<string, string>; // includes `reference` keys
raw?: { data?: string };
}
```
Expand All @@ -174,17 +172,17 @@ interface SolanaParseResult {
interface StellarParseResult {
type: "stellar";
operation?: "pay" | "tx";
address?: string; // destination (pay) or undefined (tx)
address?: string; // destination (pay) or undefined (tx)
amount?: string;
asset?: { code?: string; issuer?: string };
memo?: string;
memo_type?: string;
callback?: string;
message?: string; // mapped from `msg` param
message?: string; // mapped from `msg` param
network_passphrase?: string;
origin_domain?: string;
signature?: string;
chain_id?: number; // 1500 for Stellar mainnet (pay only)
chain_id?: number; // 1500 for Stellar mainnet (pay only)
extra_params?: Record<string, string>;
raw?: { xdr?: string; data?: string };
}
Expand Down Expand Up @@ -287,7 +285,9 @@ parseDeeplink(
// }

// Stellar SEP-7 tx
parseDeeplink("web+stellar:tx?xdr=AAAA...&network_passphrase=Public%20Global%20Stellar%20Network%20%3B%20September%202015");
parseDeeplink(
"web+stellar:tx?xdr=AAAA...&network_passphrase=Public%20Global%20Stellar%20Network%20%3B%20September%202015"
);
// {
// type: "stellar",
// operation: "tx",
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rozoai/deeplink-core",
"version": "1.1.4",
"version": "1.1.7",
"description": "Core library for deeplink",
"repository": {
"type": "git",
Expand All @@ -20,6 +20,7 @@
},
"scripts": {
"build": "vite build",
"prepublishOnly": "pnpm run build",
"test": "vitest",
"clean": "rm -rf dist && rm -rf node_modules",
"bumpp": "bumpp",
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/handlers/address.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StrKey } from "@stellar/stellar-sdk";
import { getAddress, isAddress } from "viem";
import { parseEthereum } from "../protocols/ethereum";
import { isValidSolanaAddress } from "../protocols/solana";
import { isValidStellarAddress } from "../protocols/stellar";
import type { DeeplinkData } from "../types";

/**
Expand All @@ -22,7 +22,8 @@ export function parseAddress(input: string): DeeplinkData | null {
};
}

if (isValidStellarAddress(input)) {
// G-addresses are accounts (payable); C-addresses are contracts (not payable)
if (StrKey.isValidEd25519PublicKey(input)) {
return {
type: "stellar",
address: input,
Expand All @@ -33,6 +34,16 @@ export function parseAddress(input: string): DeeplinkData | null {
};
}

if (StrKey.isValidContract(input)) {
return {
type: "stellar",
address: input,
raw: {
data: input,
},
};
}

// Handle ethereum: prefix with optional chain specification
if (input.startsWith("ethereum:")) {
try {
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/protocols/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ export function parseStellar(input: string): StellarParseResult | null {
}
}

// Handle plain Stellar address format
if (!StrKey.isValidEd25519PublicKey(input)) {
// Handle plain Stellar address format (G = account, C = contract)
if (
!StrKey.isValidEd25519PublicKey(input) &&
!StrKey.isValidContract(input)
) {
return null;
}

Expand All @@ -40,7 +43,9 @@ export function parseStellar(input: string): StellarParseResult | null {
}

export function isValidStellarAddress(address: string): boolean {
return StrKey.isValidEd25519PublicKey(address);
return (
StrKey.isValidEd25519PublicKey(address) || StrKey.isValidContract(address)
);
}

function parsePayOperation(
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/tests/address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ describe("parseAddress", () => {
expect("address" in result ? result.address : "").toBe(input);
});

it("should correctly parse a Stellar contract address (C address) without operation pay", () => {
const input = "CAHFUV2MVZ3N4VUNQ7RHECTI337SQ2D3QG7TRDSH37ND6M43D6NZANRR";
const result = parseAddress(input);
expect(result).not.toBeNull();
if (!result) return;
expect(result.type).toBe("stellar");
expect(result).not.toHaveProperty("operation", "pay");
expect("address" in result ? result.address : "").toBe(input);
});

it("should return null for an invalid address", () => {
const input = "this-is-not-a-valid-address";
const result = parseAddress(input);
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/tests/stellar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,16 @@ describe("Stellar Parser", () => {
});
});

it("should parse Stellar contract address (C address)", () => {
const input = "CAHFUV2MVZ3N4VUNQ7RHECTI337SQ2D3QG7TRDSH37ND6M43D6NZANRR";
const result = parseStellar(input) as StellarParseResult;
expect(result).not.toBeNull();
expect(result).toMatchObject({
type: "stellar",
address: input,
});
});

it("should reject invalid address format", () => {
const input = "INVALID_STELLAR_ADDRESS";
const result = parseStellar(input);
Expand Down
5 changes: 3 additions & 2 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rozoai/deeplink-react-native",
"version": "1.1.4",
"version": "1.1.7",
"description": "React Native component for scanning QR codes with Expo",
"repository": {
"type": "git",
Expand All @@ -18,6 +18,7 @@
},
"scripts": {
"build": "tsc",
"prepublishOnly": "pnpm run build",
"clean": "rm -rf dist && rm -rf node_modules",
"bumpp": "bumpp",
"type-check": "tsc --noEmit"
Expand Down Expand Up @@ -45,7 +46,7 @@
"expo-camera": ">=15"
},
"dependencies": {
"@rozoai/deeplink-core": "1.1.4"
"@rozoai/deeplink-core": "1.1.7"
},
"devDependencies": {
"@types/react": "^19.1.8",
Expand Down
5 changes: 3 additions & 2 deletions packages/scan-qr/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rozoai/deeplink-react",
"version": "1.1.4",
"version": "1.1.7",
"description": "React component for scanning QR codes",
"repository": {
"type": "git",
Expand All @@ -20,6 +20,7 @@
"scripts": {
"build": "vite build",
"dev": "vite dev",
"prepublishOnly": "pnpm run build",
"clean": "rm -rf dist && rm -rf node_modules",
"bumpp": "bumpp",
"prepare": "pnpm run build"
Expand All @@ -42,7 +43,7 @@
"react-dom": ">=18"
},
"dependencies": {
"@rozoai/deeplink-core": "1.1.4",
"@rozoai/deeplink-core": "1.1.7",
"@yudiel/react-qr-scanner": "^2.3.1"
},
"files": [
Expand Down
Loading
Loading