Skip to content

Commit 407e19a

Browse files
Implement Wallet-Based Authentication with Stellar Wallet Kit (#20)
* integrated the stellar wallet * added functions for register and login with wallet * added connect wallet function for login with wallet button * updated changes as requested in review * added compoent for the wallet component * updated the changes with the requested changes * removed imports not needed * adjusted the code according to previous review * removed yarn lock file * refactor(walletStore): clean up code and improve transaction signing logic * feat: update stellar-wallets-kit dependency and enhance wallet user confirmation flow * refactor(walletStore): simplify network type assignment in wallet creation --------- Co-authored-by: Elliot Voris <elliot@stellar.org>
1 parent 7b2b921 commit 407e19a

8 files changed

Lines changed: 1600 additions & 65 deletions

File tree

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,8 @@
4848
"uuid": "^10.0.0",
4949
"vite": "^5.4.8"
5050
},
51-
"type": "module"
52-
}
51+
"type": "module",
52+
"dependencies": {
53+
"@creit.tech/stellar-wallets-kit": "^1.2.3"
54+
}
55+
}

src/lib/components/ConfirmationModal.svelte

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ on the following occasions:
3636
// A Svelte "context" is used to control when to `open` and `close` a given
3737
// modal from within other components
3838
import { getContext } from 'svelte'
39+
import { get } from 'svelte/store'
3940
const { close } = getContext('simple-modal')
4041
4142
// `onConfirm` is a dummy function that will be overridden from the
@@ -49,27 +50,34 @@ on the following occasions:
4950
isWaiting = true
5051
5152
try {
52-
// We make sure the user has supplied the correct pincode
53-
await walletStore.confirmPincode({
54-
pincode: pincode,
55-
firstPincode: firstPincode,
56-
signup: firstPincode ? true : false,
57-
})
53+
// Check if this is a wallet user
54+
const { keyId, publicKey } = get(walletStore)
55+
const isWalletUser = keyId === publicKey
56+
57+
if (!isWalletUser) {
58+
// Only verify pincode for non-wallet users
59+
await walletStore.confirmPincode({
60+
pincode: pincode,
61+
firstPincode: firstPincode,
62+
signup: firstPincode ? true : false,
63+
})
64+
}
5865
5966
// We call the `onConfirm` function that was given to the modal by
6067
// the outside component. This method allows each page that needs to
6168
// display a modal to independantly customize the behavior that
6269
// should take place when the pincode is confirmed. (i.e., submit
6370
// the transaction to the network, login to the app, etc.)
6471
// @ts-ignore
65-
await onConfirm(pincode)
72+
// Pass pincode only for non-wallet users
73+
await onConfirm(isWalletUser ? undefined : pincode)
6674
6775
// Now we can close the modal window
6876
close()
6977
} catch (err) {
7078
// If there was an error, we set our `errorMessage` alert
7179
// @ts-ignore
72-
errorMessage.set(err.body.message)
80+
errorMessage.set(err.body?.message || err.message || 'Transaction failed')
7381
}
7482
isWaiting = false
7583
}
@@ -112,6 +120,9 @@ on the following occasions:
112120
let isWaiting = false
113121
let pincode = ''
114122
123+
// Get wallet status
124+
$: isWalletUser = get(walletStore).keyId === get(walletStore).publicKey
125+
115126
// The `$: variableName` syntax marks the output of some **expression** (as
116127
// opposed to an assignment) as _reactive_. In this case, every time
117128
// `transactionXDR` or `transactionNetwork` changes, `transaction` will be
@@ -141,8 +152,8 @@ on the following occasions:
141152
>{transaction.memo.type === 'text'
142153
? transaction.memo?.value?.toString('utf-8')
143154
: transaction.memo.type === 'hash'
144-
? transaction.memo?.value?.toString('base64')
145-
: transaction.memo.value}</code
155+
? transaction.memo?.value?.toString('base64')
156+
: transaction.memo.value}</code
146157
>
147158
</p>
148159
{/if}
@@ -185,7 +196,7 @@ on the following occasions:
185196
<ErrorAlert />
186197
187198
<!-- Display the pincode form: the input element, and the "confirm" and "reject" buttons -->
188-
{#if hasPincodeForm}
199+
{#if hasPincodeForm && !isWalletUser}
189200
<form>
190201
<div class="form-control">
191202
<label class="label" for="pincode">
@@ -216,5 +227,20 @@ on the following occasions:
216227
</button>
217228
</div>
218229
</form>
230+
{:else if isWalletUser}
231+
<!-- Wallet user confirmation UI -->
232+
<div class="my-6 flex justify-end gap-3">
233+
<button
234+
on:click|preventDefault={_onConfirm}
235+
class="btn-success btn"
236+
disabled={isWaiting}
237+
>
238+
{#if isWaiting}<span class="loading loading-spinner loading-sm" />{/if}
239+
Confirm in Wallet
240+
</button>
241+
<button on:click|preventDefault={_onReject} class="btn-error btn" disabled={isWaiting}>
242+
{rejectButton}
243+
</button>
244+
</div>
219245
{/if}
220246
</div>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!-- src/lib/components/WalletComponent.svelte -->
2+
<script>
3+
import { goto } from '$app/navigation';
4+
import { walletStore } from '$lib/stores/walletStore';
5+
// @ts-ignore
6+
import { StellarWalletsKit, WalletNetwork, allowAllModules, XBULL_ID } from '@creit.tech/stellar-wallets-kit'
7+
8+
9+
10+
export let buttonText = 'Connect Wallet';
11+
const kit = new StellarWalletsKit({
12+
network: WalletNetwork.TESTNET,
13+
selectedWalletId: XBULL_ID,
14+
modules: allowAllModules(),
15+
});
16+
17+
const connectWallet = async () => {
18+
try {
19+
await kit.openModal({
20+
// @ts-ignore
21+
onWalletSelected: async (option) => {
22+
kit.setWallet(option.id);
23+
const { address } = await kit.getAddress();
24+
25+
if (address) {
26+
await walletStore.connectWallet({ publicKey: address });
27+
goto('/dashboard');
28+
}
29+
}
30+
});
31+
} catch (error) {
32+
console.error('Error connecting wallet:', error);
33+
}
34+
};
35+
36+
export { connectWallet };
37+
</script>
38+
39+
<!-- You can provide a button or any UI elements if needed -->
40+
<button type="button" class="btn-secondary btn" on:click={connectWallet}>
41+
{buttonText}
42+
</button>

src/lib/stores/walletStore.js

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import { get } from 'svelte/store'
33
import { persisted } from 'svelte-local-storage-store'
44
import { KeyManager, LocalStorageKeyStore, ScryptEncrypter, KeyType } from '@stellar/typescript-wallet-sdk-km'
55
import { TransactionBuilder } from '@stellar/stellar-sdk'
6+
import {
7+
StellarWalletsKit,
8+
WalletNetwork,
9+
allowAllModules,
10+
XBULL_ID
11+
} from '@creit.tech/stellar-wallets-kit';
12+
613

714
/** @typedef {import('@stellar/stellar-sdk').Transaction} Transaction */
815

@@ -16,11 +23,30 @@ import { TransactionBuilder } from '@stellar/stellar-sdk'
1623

1724
function createWalletStore() {
1825
/** @type {import('svelte/store').Writable<WalletStore>} */
19-
const { subscribe, set } = persisted('bpa:walletStore', { keyId: '', publicKey: '' })
20-
26+
const { subscribe, set, } = persisted('bpa:walletStore', { keyId: '', publicKey: '' })
2127
return {
2228
subscribe,
2329

30+
31+
32+
/**
33+
* Connects a user by their public key (wallet-based registration)
34+
* @param {Object} opts Options object
35+
* @param {string} opts.publicKey Public Stellar address
36+
*/
37+
connectWallet: async ({ publicKey }) => {
38+
try {
39+
// This effectively both "registers" and "logs in" the wallet
40+
set({
41+
keyId: publicKey,
42+
publicKey: publicKey,
43+
})
44+
} catch (err) {
45+
console.error('Error connecting wallet', err)
46+
throw error(400, { message: 'Failed to connect wallet' })
47+
}
48+
},
49+
2450
/**
2551
* Registers a user by storing their encrypted keypair in the browser's localStorage.
2652
* @param {Object} opts Options object
@@ -57,7 +83,7 @@ function createWalletStore() {
5783
console.error('Error saving key', err)
5884
// @ts-ignore
5985
throw error(400, { message: err.toString() })
60-
}
86+
}
6187
},
6288

6389
/**
@@ -95,19 +121,46 @@ function createWalletStore() {
95121
*/
96122
sign: async ({ transactionXDR, network, pincode }) => {
97123
try {
98-
const keyManager = setupKeyManager()
99-
let signedTransaction = await keyManager.signTransaction({
124+
125+
const { keyId, publicKey } = get(walletStore);
126+
127+
if (keyId === publicKey) {
128+
129+
const kit = new StellarWalletsKit({
100130
// @ts-ignore
101-
transaction: TransactionBuilder.fromXDR(transactionXDR, network),
102-
id: get(walletStore).keyId,
103-
password: pincode,
104-
})
105-
return signedTransaction
106-
} catch (err) {
131+
network: network,
132+
selectedWalletId: XBULL_ID,
133+
modules: allowAllModules(),
134+
});
135+
const { address } = await kit.getAddress();
136+
137+
// Sign the transaction using the wallet address
138+
const { signedTxXdr } = await kit.signTransaction(transactionXDR, {
139+
address,
140+
networkPassphrase: network, // or use your specific network passphrase
141+
});
142+
143+
// @ts-ignore
144+
return signedTxXdr; // Return the signed transaction
145+
} else {
146+
147+
const keyManager = setupKeyManager();
148+
// Fallback to signing with pincode if no wallet
149+
const signedTransaction = await keyManager.signTransaction({
150+
// @ts-ignore
151+
transaction: TransactionBuilder.fromXDR(transactionXDR, network),
152+
id: keyId,
153+
password: pincode,
154+
});
155+
// @ts-ignore
156+
return signedTransaction;
157+
}
158+
}catch (err) {
107159
console.error('Error signing transaction', err)
108160
// @ts-ignore
109161
throw error(400, { message: err.toString() })
110162
}
163+
111164
},
112165
}
113166
}

src/routes/dashboard/+layout.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import Navbar from './components/Navbar.svelte'
2121
import Drawer from './components/Drawer.svelte'
2222
import Footer from './components/Footer.svelte'
23-
</script>
23+
</script>
2424

2525
<div class="flex min-h-screen flex-col">
2626
<Navbar />

src/routes/login/+page.svelte

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@ for submission to the network.
2525
import { goto } from '$app/navigation'
2626
import { errorMessage } from '$lib/stores/alertsStore'
2727
import { walletStore } from '$lib/stores/walletStore'
28-
28+
import WalletKitProvider from '$lib/components/WalletKitProvider.svelte'
2929
// Define some component variables that will be used throughout the page
3030
let pincode = ''
3131
32+
33+
34+
35+
36+
37+
3238
/**
3339
* Our `login` function ensures the the user has entered a valid pincode for the encrypted keypair, and then redirects them to the dashboard page.
3440
* @async
@@ -86,6 +92,11 @@ for submission to the network.
8692
<div class="form-control mt-6">
8793
<button class="btn-primary btn">Login</button>
8894
</div>
95+
96+
97+
<div class="form-control mt-2">
98+
<WalletKitProvider buttonText='Login with wallet'/>
99+
</div>
89100
</form>
90101
</div>
91102
</div>

src/routes/signup/+page.svelte

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ circumstance.
2828
import { goto } from '$app/navigation'
2929
import { walletStore } from '$lib/stores/walletStore'
3030
import { fundWithFriendbot } from '$lib/stellar/horizonQueries'
31-
31+
import WalletKitProvider from '$lib/components/WalletKitProvider.svelte'
3232
// The `open` Svelte context is used to open the confirmation modal
3333
import { getContext } from 'svelte'
3434
const { open } = getContext('simple-modal')
@@ -40,6 +40,8 @@ circumstance.
4040
let showSecret = false
4141
let pincode = ''
4242
43+
44+
4345
/**
4446
* Takes an action after the pincode has been confirmed by the user.
4547
* @async
@@ -74,6 +76,11 @@ circumstance.
7476
onConfirm: onConfirm,
7577
})
7678
}
79+
80+
81+
82+
83+
7784
</script>
7885

7986
<div class="hero min-h-screen bg-base-200">
@@ -147,6 +154,9 @@ circumstance.
147154
<div class="form-control mt-6">
148155
<button type="submit" class="btn-primary btn">Signup</button>
149156
</div>
157+
<div class="form-control mt-2">
158+
<WalletKitProvider buttonText='Sign up with wallet'/>
159+
</div>
150160
<div class="form-control my-1">
151161
<div class="label">
152162
<a class="link-hover label-text-alt link" href="/login">

0 commit comments

Comments
 (0)