create-openfort 0.1.10 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/index.js +20 -20
  3. package/package.json +1 -1
  4. package/template/openfort-templates/firebase/AGENTS.md +1 -1
  5. package/template/openfort-templates/firebase/package.json +3 -3
  6. package/template/openfort-templates/firebase/src/App.tsx +2 -1
  7. package/template/openfort-templates/firebase/src/integrations/openfort/providers.tsx +36 -35
  8. package/template/openfort-templates/firebase/src/lib/contracts.ts +34 -0
  9. package/template/openfort-templates/firebase/src/ui/openfort/blockchain/ActionsCard.tsx +25 -13
  10. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletCreation.tsx +108 -63
  11. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletListCard.tsx +211 -41
  12. package/template/openfort-templates/firebase/src/ui/openfort/wallets/WalletPasswordSheets.tsx +45 -21
  13. package/template/openfort-templates/headless/AGENTS.md +1 -1
  14. package/template/openfort-templates/headless/package.json +2 -2
  15. package/template/openfort-templates/headless/src/components/cards/actions.tsx +30 -21
  16. package/template/openfort-templates/headless/src/components/cards/profile.tsx +0 -2
  17. package/template/openfort-templates/headless/src/components/cards/wallets.tsx +230 -67
  18. package/template/openfort-templates/headless/src/components/createWallet.tsx +115 -73
  19. package/template/openfort-templates/headless/src/components/passwordRecovery.tsx +48 -23
  20. package/template/openfort-templates/headless/src/components/providers.tsx +30 -25
  21. package/template/openfort-templates/headless/src/lib/contracts.ts +43 -0
  22. package/template/openfort-templates/openfort-ui/AGENTS.md +1 -1
  23. package/template/openfort-templates/openfort-ui/package.json +3 -3
  24. package/template/openfort-templates/openfort-ui/src/components/cards/actions.tsx +30 -15
  25. package/template/openfort-templates/openfort-ui/src/components/cards/auth.tsx +1 -1
  26. package/template/openfort-templates/openfort-ui/src/components/cards/wallets.tsx +232 -67
  27. package/template/openfort-templates/openfort-ui/src/components/createWallet.tsx +115 -73
  28. package/template/openfort-templates/openfort-ui/src/components/passwordRecovery.tsx +48 -23
  29. package/template/openfort-templates/openfort-ui/src/components/providers.tsx +34 -30
  30. package/template/openfort-templates/openfort-ui/src/lib/contracts.ts +35 -0
  31. package/template/openfort-templates/solana-headless/biome.json +70 -0
  32. package/template/openfort-templates/solana-headless/index.html +16 -0
  33. package/template/openfort-templates/solana-headless/package.json +34 -0
  34. package/template/openfort-templates/solana-headless/public/githubLogo.svg +5 -0
  35. package/template/openfort-templates/solana-headless/public/openfort.svg +13 -0
  36. package/template/openfort-templates/solana-headless/public/solanaLogo.svg +11 -0
  37. package/template/openfort-templates/solana-headless/src/App.tsx +7 -0
  38. package/template/openfort-templates/solana-headless/src/components/cards/auth.tsx +167 -0
  39. package/template/openfort-templates/solana-headless/src/components/cards/head.tsx +359 -0
  40. package/template/openfort-templates/solana-headless/src/components/cards/history.tsx +134 -0
  41. package/template/openfort-templates/solana-headless/src/components/cards/main.tsx +140 -0
  42. package/template/openfort-templates/solana-headless/src/components/cards/profile.tsx +80 -0
  43. package/template/openfort-templates/solana-headless/src/components/cards/send.tsx +242 -0
  44. package/template/openfort-templates/solana-headless/src/components/cards/sign.tsx +48 -0
  45. package/template/openfort-templates/solana-headless/src/components/cards/wallets.tsx +199 -0
  46. package/template/openfort-templates/solana-headless/src/components/createWallet.tsx +117 -0
  47. package/template/openfort-templates/solana-headless/src/components/passwordRecovery.tsx +167 -0
  48. package/template/openfort-templates/solana-headless/src/components/providers.tsx +23 -0
  49. package/template/openfort-templates/solana-headless/src/components/ui/Sheet.tsx +47 -0
  50. package/template/openfort-templates/solana-headless/src/components/ui/Tabs.tsx +111 -0
  51. package/template/openfort-templates/solana-headless/src/components/ui/TruncateData.tsx +31 -0
  52. package/template/openfort-templates/solana-headless/src/hooks/useSolanaMessageSigner.ts +37 -0
  53. package/template/openfort-templates/solana-headless/src/index.css +180 -0
  54. package/template/openfort-templates/solana-headless/src/lib/errors.ts +4 -0
  55. package/template/openfort-templates/solana-headless/src/lib/solana/balance.ts +17 -0
  56. package/template/openfort-templates/solana-headless/src/lib/solana/index.ts +4 -0
  57. package/template/openfort-templates/solana-headless/src/lib/solana/kora.ts +137 -0
  58. package/template/openfort-templates/solana-headless/src/lib/solana/transaction.ts +146 -0
  59. package/template/openfort-templates/solana-headless/src/lib/solana/transactionHistory.ts +39 -0
  60. package/template/openfort-templates/solana-headless/src/main.tsx +13 -0
  61. package/template/openfort-templates/solana-headless/src/vite-env.d.ts +1 -0
  62. package/template/openfort-templates/solana-headless/tsconfig.app.json +24 -0
  63. package/template/openfort-templates/solana-headless/tsconfig.json +7 -0
  64. package/template/openfort-templates/solana-headless/tsconfig.node.json +22 -0
  65. package/template/openfort-templates/solana-headless/vite.config.ts +8 -0
@@ -0,0 +1,137 @@
1
+ import type { OpenfortEmbeddedSolanaWalletProvider } from '@openfort/react/solana'
2
+ import {
3
+ type Address,
4
+ appendTransactionMessageInstructions,
5
+ type Blockhash,
6
+ createNoopSigner,
7
+ createTransactionMessage,
8
+ getBase64EncodedWireTransaction,
9
+ type Instruction,
10
+ type MicroLamports,
11
+ partiallySignTransactionMessageWithSigners,
12
+ type SignatureBytes,
13
+ setTransactionMessageFeePayerSigner,
14
+ setTransactionMessageLifetimeUsingBlockhash,
15
+ } from '@solana/kit'
16
+ import { KoraClient } from '@solana/kora'
17
+ import {
18
+ updateOrAppendSetComputeUnitLimitInstruction,
19
+ updateOrAppendSetComputeUnitPriceInstruction,
20
+ } from '@solana-program/compute-budget'
21
+ import { Base58 } from 'ox'
22
+
23
+ const COMPUTE_UNIT_LIMIT = 200_000
24
+ const COMPUTE_UNIT_PRICE = 50_000n as MicroLamports
25
+
26
+ interface KoraConfig {
27
+ rpcUrl: string
28
+ apiKey: string
29
+ }
30
+
31
+ interface SendGaslessTransactionParams {
32
+ from: Address
33
+ to: Address
34
+ amountInSol: number
35
+ provider: OpenfortEmbeddedSolanaWalletProvider
36
+ koraConfig: KoraConfig
37
+ }
38
+
39
+ interface GaslessTransactionResult {
40
+ signature: string
41
+ }
42
+
43
+ /** Validate that the signature is a 64-byte Ed25519 signature */
44
+ function validateEd25519Signature(raw: Uint8Array): Uint8Array {
45
+ if (raw.length !== 64) {
46
+ throw new Error(`Invalid Ed25519 signature: expected 64 bytes, got ${raw.length}`)
47
+ }
48
+ return raw
49
+ }
50
+
51
+ function buildTransactionMessage(
52
+ feePayer: ReturnType<typeof createNoopSigner>,
53
+ blockhash: string,
54
+ instructions: Instruction[]
55
+ ) {
56
+ const msg = createTransactionMessage({ version: 0 })
57
+ const withPayer = setTransactionMessageFeePayerSigner(feePayer, msg)
58
+ const withLifetime = setTransactionMessageLifetimeUsingBlockhash(
59
+ { blockhash: blockhash as Blockhash, lastValidBlockHeight: 0n },
60
+ withPayer
61
+ )
62
+ const withPrice = updateOrAppendSetComputeUnitPriceInstruction(COMPUTE_UNIT_PRICE, withLifetime)
63
+ const withLimit = updateOrAppendSetComputeUnitLimitInstruction(COMPUTE_UNIT_LIMIT, withPrice)
64
+ return appendTransactionMessageInstructions(instructions, withLimit)
65
+ }
66
+
67
+ export async function sendGaslessSolTransaction({
68
+ from,
69
+ to,
70
+ amountInSol,
71
+ provider,
72
+ koraConfig,
73
+ }: SendGaslessTransactionParams): Promise<GaslessTransactionResult> {
74
+ const client = new KoraClient({
75
+ rpcUrl: koraConfig.rpcUrl,
76
+ apiKey: koraConfig.apiKey,
77
+ })
78
+
79
+ // Step 1: Get Kora's signer address
80
+ const { signer_address } = await client.getPayerSigner()
81
+ const koraNoopSigner = createNoopSigner(signer_address as Address)
82
+
83
+ // Step 2: Create transfer instruction via Kora
84
+ const transferLamports = Math.floor(amountInSol * 1_000_000_000)
85
+ const transferSol = await client.transferTransaction({
86
+ amount: transferLamports,
87
+ token: '11111111111111111111111111111111',
88
+ source: from,
89
+ destination: to,
90
+ signer_key: signer_address,
91
+ })
92
+
93
+ // Step 3: Build transaction with Kora as fee payer
94
+ const { blockhash } = await client.getBlockhash()
95
+ const transaction = buildTransactionMessage(koraNoopSigner, blockhash, transferSol.instructions)
96
+
97
+ // Step 4: Partially sign (noop for Kora placeholder), then sign with Openfort
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ const partiallySignedTx = await partiallySignTransactionMessageWithSigners(transaction as any)
100
+
101
+ const result = await provider.signTransaction(new Uint8Array(partiallySignedTx.messageBytes))
102
+ const signatureBytes = validateEd25519Signature(Base58.toBytes(result.signature))
103
+
104
+ const userSignedTx = {
105
+ ...partiallySignedTx,
106
+ signatures: {
107
+ ...partiallySignedTx.signatures,
108
+ [from]: signatureBytes as SignatureBytes,
109
+ },
110
+ }
111
+ const userSignedWire = getBase64EncodedWireTransaction(userSignedTx)
112
+
113
+ // Step 5: Send to Kora for co-signing and submission to Solana
114
+ const response = await client.signAndSendTransaction({
115
+ transaction: userSignedWire,
116
+ signer_key: signer_address,
117
+ })
118
+
119
+ // Extract signature: Kora may return it directly or in signed_transaction wire format
120
+ let txSignature = (response as unknown as Record<string, unknown>).signature as string | undefined
121
+ if (!txSignature) {
122
+ const signedTxB64 = (response as unknown as Record<string, unknown>).signed_transaction as string | undefined
123
+ if (signedTxB64) {
124
+ const wireBytes = Uint8Array.from(atob(signedTxB64), (c) => c.charCodeAt(0))
125
+ // Wire format: [num_signatures (1 byte), ...signatures (64 bytes each), ...]
126
+ // First signature (bytes 1..65) is the tx signature
127
+ const firstSig = wireBytes.slice(1, 65)
128
+ txSignature = Base58.fromBytes(firstSig)
129
+ }
130
+ }
131
+
132
+ if (!txSignature) {
133
+ throw new Error('Failed to extract transaction signature from Kora response')
134
+ }
135
+
136
+ return { signature: txSignature }
137
+ }
@@ -0,0 +1,146 @@
1
+ import type { OpenfortEmbeddedSolanaWalletProvider } from '@openfort/react/solana'
2
+ import {
3
+ type Address,
4
+ appendTransactionMessageInstruction,
5
+ assertIsTransactionWithBlockhashLifetime,
6
+ createSolanaRpc,
7
+ createSolanaRpcSubscriptions,
8
+ createTransactionMessage,
9
+ lamports,
10
+ pipe,
11
+ type SignatureBytes,
12
+ type SignatureDictionary,
13
+ sendAndConfirmTransactionFactory,
14
+ setTransactionMessageFeePayer,
15
+ setTransactionMessageLifetimeUsingBlockhash,
16
+ signTransactionMessageWithSigners,
17
+ type TransactionSigner,
18
+ } from '@solana/kit'
19
+ import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction } from '@solana-program/compute-budget'
20
+ import { getTransferSolInstruction } from '@solana-program/system'
21
+ import { Base58 } from 'ox'
22
+
23
+ const COMPUTE_UNIT_LIMIT = 200_000
24
+ const COMPUTE_UNIT_PRICE = 50_000n // microlamports
25
+
26
+ interface SendTransactionParams {
27
+ from: Address
28
+ to: Address
29
+ amountInSol: number
30
+ provider: OpenfortEmbeddedSolanaWalletProvider
31
+ rpcUrl?: string
32
+ }
33
+
34
+ interface TransactionResult {
35
+ signature: string
36
+ }
37
+
38
+ /** Convert a decimal amount to the smallest unit (e.g. SOL to lamports) without floating-point loss */
39
+ function toSmallestUnit(amount: number, decimals: number): bigint {
40
+ const str = amount.toString()
41
+ const [whole, frac = ''] = str.split('.')
42
+ const padded = (frac + '0'.repeat(decimals)).slice(0, decimals)
43
+ return BigInt(whole + padded)
44
+ }
45
+
46
+ /** Validate that the signature is a 64-byte Ed25519 signature */
47
+ function validateEd25519Signature(raw: Uint8Array): Uint8Array {
48
+ if (raw.length !== 64) {
49
+ throw new Error(`Invalid Ed25519 signature: expected 64 bytes, got ${raw.length}`)
50
+ }
51
+ return raw
52
+ }
53
+
54
+ /** Extract the fee payer's signature from the signed transaction */
55
+ function getTransactionSignature(signatures: Record<string, SignatureBytes | null>, feePayer: Address): string {
56
+ const sig = signatures[feePayer]
57
+ if (!sig) {
58
+ throw new Error(`Fee payer signature not found for address: ${feePayer}`)
59
+ }
60
+ return Base58.fromBytes(sig)
61
+ }
62
+
63
+ function deriveWssUrl(rpcUrl: string): string {
64
+ try {
65
+ const parsed = new URL(rpcUrl)
66
+ const hostname = parsed.hostname
67
+ if (hostname === 'openfort.io' || hostname.endsWith('.openfort.io')) {
68
+ return 'wss://api.devnet.solana.com'
69
+ }
70
+ } catch {
71
+ // fall through
72
+ }
73
+ return rpcUrl.replace(/^https?:\/\//, 'wss://')
74
+ }
75
+
76
+ function createProviderSigner(
77
+ signerAddress: Address,
78
+ provider: OpenfortEmbeddedSolanaWalletProvider
79
+ ): TransactionSigner {
80
+ return {
81
+ address: signerAddress,
82
+ signTransactions: async (transactions): Promise<readonly SignatureDictionary[]> => {
83
+ return Promise.all(
84
+ transactions.map(async (transaction) => {
85
+ const result = await provider.signTransaction({
86
+ messageBytes: new Uint8Array(transaction.messageBytes),
87
+ })
88
+ const signatureBytes = validateEd25519Signature(Base58.toBytes(result.signature))
89
+ return Object.freeze({
90
+ [signerAddress]: signatureBytes as SignatureBytes,
91
+ })
92
+ })
93
+ )
94
+ },
95
+ }
96
+ }
97
+
98
+ export async function sendSolTransaction({
99
+ from,
100
+ to,
101
+ amountInSol,
102
+ provider,
103
+ rpcUrl,
104
+ }: SendTransactionParams): Promise<TransactionResult> {
105
+ const httpUrl = rpcUrl ?? 'https://api.devnet.solana.com'
106
+ const rpc = createSolanaRpc(httpUrl)
107
+ const rpcSubscriptions = createSolanaRpcSubscriptions(deriveWssUrl(httpUrl))
108
+
109
+ const signer = createProviderSigner(from, provider)
110
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send()
111
+
112
+ const transactionMessage = pipe(
113
+ createTransactionMessage({ version: 0 }),
114
+ (tx) => setTransactionMessageFeePayer(from, tx),
115
+ (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
116
+ (tx) => appendTransactionMessageInstruction(getSetComputeUnitLimitInstruction({ units: COMPUTE_UNIT_LIMIT }), tx),
117
+ (tx) =>
118
+ appendTransactionMessageInstruction(getSetComputeUnitPriceInstruction({ microLamports: COMPUTE_UNIT_PRICE }), tx),
119
+ (tx) =>
120
+ appendTransactionMessageInstruction(
121
+ getTransferSolInstruction({
122
+ source: signer,
123
+ destination: to,
124
+ amount: lamports(toSmallestUnit(amountInSol, 9)),
125
+ }),
126
+ tx
127
+ )
128
+ )
129
+
130
+ const signedTransaction = await signTransactionMessageWithSigners(transactionMessage)
131
+ assertIsTransactionWithBlockhashLifetime(signedTransaction)
132
+
133
+ const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })
134
+ const abortController = new AbortController()
135
+ const timeout = setTimeout(() => abortController.abort(), 60_000)
136
+ try {
137
+ await sendAndConfirmTransaction(signedTransaction, {
138
+ commitment: 'confirmed',
139
+ abortSignal: abortController.signal,
140
+ })
141
+ } finally {
142
+ clearTimeout(timeout)
143
+ }
144
+
145
+ return { signature: getTransactionSignature(signedTransaction.signatures, from) }
146
+ }
@@ -0,0 +1,39 @@
1
+ interface TransactionHistoryItem {
2
+ signature: string
3
+ slot: number
4
+ blockTime: number | null
5
+ err: unknown | null
6
+ memo: string | null
7
+ }
8
+
9
+ const DEFAULT_RPC = 'https://api.devnet.solana.com'
10
+
11
+ /**
12
+ * Fetches tx history via raw JSON-RPC (avoids @solana/kit transport issues in browser).
13
+ */
14
+ export async function getTransactionHistory(
15
+ address: string,
16
+ limit = 10,
17
+ rpcUrl?: string
18
+ ): Promise<TransactionHistoryItem[]> {
19
+ const rpc = rpcUrl ?? DEFAULT_RPC
20
+ const res = await fetch(rpc, {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify({
24
+ jsonrpc: '2.0',
25
+ id: 1,
26
+ method: 'getSignaturesForAddress',
27
+ params: [address, { limit, commitment: 'confirmed' }],
28
+ }),
29
+ })
30
+ const data = await res.json()
31
+ if (!data.result) return []
32
+ return data.result.map((sig: Record<string, unknown>) => ({
33
+ signature: sig.signature as string,
34
+ slot: sig.slot as number,
35
+ blockTime: (sig.blockTime as number) ?? null,
36
+ err: sig.err ?? null,
37
+ memo: (sig.memo as string) ?? null,
38
+ }))
39
+ }
@@ -0,0 +1,13 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App.tsx'
4
+ import { Providers } from './components/providers.tsx'
5
+ import './index.css'
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <StrictMode>
9
+ <Providers>
10
+ <App />
11
+ </Providers>
12
+ </StrictMode>,
13
+ )
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ /* Linting */
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "erasableSyntaxOnly": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["src"]
24
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ /* Bundler mode */
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ /* Linting */
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "erasableSyntaxOnly": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "noUncheckedSideEffectImports": true
20
+ },
21
+ "include": ["vite.config.ts"]
22
+ }
@@ -0,0 +1,8 @@
1
+ import tailwindcss from '@tailwindcss/vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import { defineConfig } from 'vite'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ })