Skip to content

Commit

Permalink
Implement Wallet-Based Authentication with Stellar Wallet Kit (#20)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
josephchimebuka and ElliotFriend authored Nov 8, 2024
1 parent 7b2b921 commit 407e19a
Show file tree
Hide file tree
Showing 8 changed files with 1,600 additions and 65 deletions.
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,8 @@
"uuid": "^10.0.0",
"vite": "^5.4.8"
},
"type": "module"
}
"type": "module",
"dependencies": {
"@creit.tech/stellar-wallets-kit": "^1.2.3"
}
}
48 changes: 37 additions & 11 deletions src/lib/components/ConfirmationModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ on the following occasions:
// A Svelte "context" is used to control when to `open` and `close` a given
// modal from within other components
import { getContext } from 'svelte'
import { get } from 'svelte/store'
const { close } = getContext('simple-modal')
// `onConfirm` is a dummy function that will be overridden from the
Expand All @@ -49,27 +50,34 @@ on the following occasions:
isWaiting = true
try {
// We make sure the user has supplied the correct pincode
await walletStore.confirmPincode({
pincode: pincode,
firstPincode: firstPincode,
signup: firstPincode ? true : false,
})
// Check if this is a wallet user
const { keyId, publicKey } = get(walletStore)
const isWalletUser = keyId === publicKey
if (!isWalletUser) {
// Only verify pincode for non-wallet users
await walletStore.confirmPincode({
pincode: pincode,
firstPincode: firstPincode,
signup: firstPincode ? true : false,
})
}
// We call the `onConfirm` function that was given to the modal by
// the outside component. This method allows each page that needs to
// display a modal to independantly customize the behavior that
// should take place when the pincode is confirmed. (i.e., submit
// the transaction to the network, login to the app, etc.)
// @ts-ignore
await onConfirm(pincode)
// Pass pincode only for non-wallet users
await onConfirm(isWalletUser ? undefined : pincode)
// Now we can close the modal window
close()
} catch (err) {
// If there was an error, we set our `errorMessage` alert
// @ts-ignore
errorMessage.set(err.body.message)
errorMessage.set(err.body?.message || err.message || 'Transaction failed')
}
isWaiting = false
}
Expand Down Expand Up @@ -112,6 +120,9 @@ on the following occasions:
let isWaiting = false
let pincode = ''
// Get wallet status
$: isWalletUser = get(walletStore).keyId === get(walletStore).publicKey
// The `$: variableName` syntax marks the output of some **expression** (as
// opposed to an assignment) as _reactive_. In this case, every time
// `transactionXDR` or `transactionNetwork` changes, `transaction` will be
Expand Down Expand Up @@ -141,8 +152,8 @@ on the following occasions:
>{transaction.memo.type === 'text'
? transaction.memo?.value?.toString('utf-8')
: transaction.memo.type === 'hash'
? transaction.memo?.value?.toString('base64')
: transaction.memo.value}</code
? transaction.memo?.value?.toString('base64')
: transaction.memo.value}</code
>
</p>
{/if}
Expand Down Expand Up @@ -185,7 +196,7 @@ on the following occasions:
<ErrorAlert />
<!-- Display the pincode form: the input element, and the "confirm" and "reject" buttons -->
{#if hasPincodeForm}
{#if hasPincodeForm && !isWalletUser}
<form>
<div class="form-control">
<label class="label" for="pincode">
Expand Down Expand Up @@ -216,5 +227,20 @@ on the following occasions:
</button>
</div>
</form>
{:else if isWalletUser}
<!-- Wallet user confirmation UI -->
<div class="my-6 flex justify-end gap-3">
<button
on:click|preventDefault={_onConfirm}
class="btn-success btn"
disabled={isWaiting}
>
{#if isWaiting}<span class="loading loading-spinner loading-sm" />{/if}
Confirm in Wallet
</button>
<button on:click|preventDefault={_onReject} class="btn-error btn" disabled={isWaiting}>
{rejectButton}
</button>
</div>
{/if}
</div>
42 changes: 42 additions & 0 deletions src/lib/components/WalletKitProvider.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!-- src/lib/components/WalletComponent.svelte -->
<script>
import { goto } from '$app/navigation';
import { walletStore } from '$lib/stores/walletStore';
// @ts-ignore
import { StellarWalletsKit, WalletNetwork, allowAllModules, XBULL_ID } from '@creit.tech/stellar-wallets-kit'
export let buttonText = 'Connect Wallet';
const kit = new StellarWalletsKit({
network: WalletNetwork.TESTNET,
selectedWalletId: XBULL_ID,
modules: allowAllModules(),
});
const connectWallet = async () => {
try {
await kit.openModal({
// @ts-ignore
onWalletSelected: async (option) => {
kit.setWallet(option.id);
const { address } = await kit.getAddress();
if (address) {
await walletStore.connectWallet({ publicKey: address });
goto('/dashboard');
}
}
});
} catch (error) {
console.error('Error connecting wallet:', error);
}
};
export { connectWallet };
</script>

<!-- You can provide a button or any UI elements if needed -->
<button type="button" class="btn-secondary btn" on:click={connectWallet}>
{buttonText}
</button>
75 changes: 64 additions & 11 deletions src/lib/stores/walletStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { get } from 'svelte/store'
import { persisted } from 'svelte-local-storage-store'
import { KeyManager, LocalStorageKeyStore, ScryptEncrypter, KeyType } from '@stellar/typescript-wallet-sdk-km'
import { TransactionBuilder } from '@stellar/stellar-sdk'
import {
StellarWalletsKit,
WalletNetwork,
allowAllModules,
XBULL_ID
} from '@creit.tech/stellar-wallets-kit';


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

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

function createWalletStore() {
/** @type {import('svelte/store').Writable<WalletStore>} */
const { subscribe, set } = persisted('bpa:walletStore', { keyId: '', publicKey: '' })

const { subscribe, set, } = persisted('bpa:walletStore', { keyId: '', publicKey: '' })
return {
subscribe,



/**
* Connects a user by their public key (wallet-based registration)
* @param {Object} opts Options object
* @param {string} opts.publicKey Public Stellar address
*/
connectWallet: async ({ publicKey }) => {
try {
// This effectively both "registers" and "logs in" the wallet
set({
keyId: publicKey,
publicKey: publicKey,
})
} catch (err) {
console.error('Error connecting wallet', err)
throw error(400, { message: 'Failed to connect wallet' })
}
},

/**
* Registers a user by storing their encrypted keypair in the browser's localStorage.
* @param {Object} opts Options object
Expand Down Expand Up @@ -57,7 +83,7 @@ function createWalletStore() {
console.error('Error saving key', err)
// @ts-ignore
throw error(400, { message: err.toString() })
}
}
},

/**
Expand Down Expand Up @@ -95,19 +121,46 @@ function createWalletStore() {
*/
sign: async ({ transactionXDR, network, pincode }) => {
try {
const keyManager = setupKeyManager()
let signedTransaction = await keyManager.signTransaction({

const { keyId, publicKey } = get(walletStore);

if (keyId === publicKey) {

const kit = new StellarWalletsKit({
// @ts-ignore
transaction: TransactionBuilder.fromXDR(transactionXDR, network),
id: get(walletStore).keyId,
password: pincode,
})
return signedTransaction
} catch (err) {
network: network,
selectedWalletId: XBULL_ID,
modules: allowAllModules(),
});
const { address } = await kit.getAddress();

// Sign the transaction using the wallet address
const { signedTxXdr } = await kit.signTransaction(transactionXDR, {
address,
networkPassphrase: network, // or use your specific network passphrase
});

// @ts-ignore
return signedTxXdr; // Return the signed transaction
} else {

const keyManager = setupKeyManager();
// Fallback to signing with pincode if no wallet
const signedTransaction = await keyManager.signTransaction({
// @ts-ignore
transaction: TransactionBuilder.fromXDR(transactionXDR, network),
id: keyId,
password: pincode,
});
// @ts-ignore
return signedTransaction;
}
}catch (err) {
console.error('Error signing transaction', err)
// @ts-ignore
throw error(400, { message: err.toString() })
}

},
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/dashboard/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import Navbar from './components/Navbar.svelte'
import Drawer from './components/Drawer.svelte'
import Footer from './components/Footer.svelte'
</script>
</script>

<div class="flex min-h-screen flex-col">
<Navbar />
Expand Down
13 changes: 12 additions & 1 deletion src/routes/login/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,16 @@ for submission to the network.
import { goto } from '$app/navigation'
import { errorMessage } from '$lib/stores/alertsStore'
import { walletStore } from '$lib/stores/walletStore'
import WalletKitProvider from '$lib/components/WalletKitProvider.svelte'
// Define some component variables that will be used throughout the page
let pincode = ''
/**
* Our `login` function ensures the the user has entered a valid pincode for the encrypted keypair, and then redirects them to the dashboard page.
* @async
Expand Down Expand Up @@ -86,6 +92,11 @@ for submission to the network.
<div class="form-control mt-6">
<button class="btn-primary btn">Login</button>
</div>


<div class="form-control mt-2">
<WalletKitProvider buttonText='Login with wallet'/>
</div>
</form>
</div>
</div>
Expand Down
12 changes: 11 additions & 1 deletion src/routes/signup/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ circumstance.
import { goto } from '$app/navigation'
import { walletStore } from '$lib/stores/walletStore'
import { fundWithFriendbot } from '$lib/stellar/horizonQueries'
import WalletKitProvider from '$lib/components/WalletKitProvider.svelte'
// The `open` Svelte context is used to open the confirmation modal
import { getContext } from 'svelte'
const { open } = getContext('simple-modal')
Expand All @@ -40,6 +40,8 @@ circumstance.
let showSecret = false
let pincode = ''
/**
* Takes an action after the pincode has been confirmed by the user.
* @async
Expand Down Expand Up @@ -74,6 +76,11 @@ circumstance.
onConfirm: onConfirm,
})
}
</script>

<div class="hero min-h-screen bg-base-200">
Expand Down Expand Up @@ -147,6 +154,9 @@ circumstance.
<div class="form-control mt-6">
<button type="submit" class="btn-primary btn">Signup</button>
</div>
<div class="form-control mt-2">
<WalletKitProvider buttonText='Sign up with wallet'/>
</div>
<div class="form-control my-1">
<div class="label">
<a class="link-hover label-text-alt link" href="/login">
Expand Down
Loading

0 comments on commit 407e19a

Please sign in to comment.