Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.
/ wallet-sdk Public archive

The Wallet SDK for dApp developers building on Radix.

License

Notifications You must be signed in to change notification settings

radixdlt/wallet-sdk

Repository files navigation

This repository is archived

Please check Radix dApp Toolkit repository for most up to date code


License

This is a TypeScript developer SDK that facilitates communication with the Radix Wallet for two purposes: requesting various forms of data from the wallet and sending transactions to the wallet.

Important Note: This is an early release for development on the Radix Betanet and the Radix Wallet developer preview. This readme describes the intended full interface for the Radix mainnet release, but many features are not yet available (and are flagged as such).

The current version only supports desktop browser webapps with requests made via the Radix Wallet Connector browser extension. It is intended to later add support for mobile browser webapps using deep linking with the same essential interface.

You may wish to consider using this with dApp toolkit, which works with this SDK to provide additional features for your application and users.

Installation

Using NPM

npm install @radixdlt/wallet-sdk

Using Yarn

yarn add @radixdlt/wallet-sdk

Getting started

import { WalletSdk } from '@radixdlt/wallet-sdk'

const walletSdk = WalletSdk({
  networkId: 12,
  dAppDefinitionAddress:
    'account_tdx_c_1p8j5r3umpgdwpedqssn0mwnwj9tv7ae7wfzjd9srwh5q9stufq',
})
type Metadata = {
  networkId: number
  dAppDefinitionAddress: string
}
Network ID
Mainnet 1
RCNet-V1 12
  • requires networkId - Specifies which network to use
  • requires dAppDefinitionAddress - Specifies the dApp that is interacting with the wallet. Used in dApp verification process on the wallet side.

⬇️ Getting Wallet Data

About oneTime VS ongoing requests

There are two types of data requests: oneTime and ongoing.

OneTime data requests will always result in the Radix Wallet asking for the user's permission to share the data with the dApp.

type WalletUnauthorizedRequestItems = {
  discriminator: 'unauthorizedRequest'
  oneTimeAccounts?: AccountsRequestItem
  oneTimePersonaData?: PersonaDataRequestItem
}

Ongoing data requests will only result in the Radix Wallet asking for the user's permission the first time. If accepted, the Radix Wallet will automatically respond to future data requests of this type with the current data. The user's permissions for ongoing data sharing with a given dApp can be managed or revoked by the user at any time in the Radix Wallet.

type WalletAuthorizedRequestItems = {
  discriminator: 'authorizedRequest'
  auth: AuthRequestItem
  reset?: ResetRequestItem
  oneTimeAccounts?: AccountsRequestItem
  oneTimePersonaData?: PersonaDataRequestItem
  ongoingAccounts?: AccountsRequestItem
  ongoingPersonaData?: PersonaDataRequestItem
}

The user's ongoing data sharing permissions are associated with a given Persona (similar to a login) in the Radix Wallet. This means that in order to request ongoing data, a identityAddress must be included.

Typically the dApp should begin with a login request which will return the identityAddress for the user's chosen Persona, which can be used for further requests (perhaps while the user has a valid session)

πŸ’Ά Accounts

This request type is for getting one or more Radix accounts managed by the user's Radix Wallet app. You may specify the number of accounts desired, and if you require proof of ownership of the account.

Request

type NumberOfValues = {
  quantifier: 'exactly' | 'atLeast'
  quantity: number
}
type AccountsRequestItem = {
  challenge?: Challenge
  numberOfAccounts: NumberOfValues
}

Response

type Account = {
  address: string
  label: string
  appearanceId: number
}
type AccountProof = {
  accountAddress: string
  proof: Proof
}
type Proof = {
  publicKey: string
  signature: string
  curve: 'curve25519' | 'secp256k1'
}
type AccountsRequestResponseItem = {
  accounts: Account[]
  challenge?: Challenge
  proofs?: AccountProof[]
}
ongoingAccounts example
const result = await sdk.request({
  discriminator: 'authorizedRequest',
  auth: { discriminator: 'loginWithoutChallenge' },
  ongoingAccounts: {
    numberOfAccounts: { quantifier: 'atLeast', quantity: 1 },
  },
})

if (result.isErr()) {
  // code to handle the exception
}

// {
//   discriminator: "authorizedRequest",
//   auth: {
//     discriminator: loginWithoutChallenge,
//     persona: Persona
//   },
//   ongoingAccounts: {
//     accounts: Account[]
//   }
// }
const value = result.value
oneTimeAccounts example
const result = await walletSdk.request({
  discriminator: 'unauthorizedRequest',
  oneTimeAccounts: { numberOfAccounts: { quantifier: 'atLeast', quantity: 1 } },
})

if (result.isErr()) {
  // code to handle the exception
}

// {
//   discriminator: "unauthorizedRequest",
//   oneTimeAccounts: {
//     accounts: Account[]
//   }
// }
const value = result.value
with proof of ownership example
// hex encoded 32 random bytes
const challenge = [...crypto.getRandomValues(new Uint8Array(32))]
  .map((item) => item.toString(16).padStart(2, '0'))
  .join('')

const result = await sdk.request({
  discriminator: 'authorizedRequest',
  auth: { discriminator: 'loginWithoutChallenge' },
  ongoingAccounts: {
    challenge,
    numberOfAccounts: { quantifier: 'atLeast', quantity: 1 },
  },
  oneTimeAccounts: {
    challenge,
    numberOfAccounts: { quantifier: 'atLeast', quantity: 1 },
  },
})

if (result.isErr()) {
  // code to handle the exception
}

// {
//   discriminator: "authorizedRequest",
//   auth: {
//     discriminator: loginWithoutChallenge,
//     persona: Persona
//   },
//   ongoingAccounts: {
//     accounts: Account[],
//     challenge,
//     proofs: AccountProof[]
//   },
//   oneTimeAccounts: {
//     accounts: Account[],
//     challenge,
//     proofs: AccountProof[]
//   }
// }
const value = result.value

ℹ️ Persona Data

This request type is for a list of personal data fields associated with the user's selected Persona.

Request

type PersonaDataRequestItem = {
  isRequestingName?: boolean()
  numberOfRequestedEmailAddresses?: NumberOfValues
  numberOfRequestedPhoneNumbers?: NumberOfValues
}

Response

type PersonaDataRequestResponseItem = {
  name?: {
    variant: 'eastern' | 'western'
    family: string
    given: string
  }
  emailAddresses?: NumberOfValues
  phoneNumbers?: NumberOfValues
}
ongoingPersonaData example
const result = await sdk.request({
  discriminator: 'authorizedRequest',
  auth: { discriminator: 'loginWithoutChallenge' },
  ongoingPersonaData: {
    isRequestingName: true,
    numberOfRequestedEmailAddresses: { quantifier: 'atLeast', quantity: 1 },
    numberOfRequestedPhoneNumbers: { quantifier: 'exactly', quantity: 1 },
  },
})

if (result.isErr()) {
  // code to handle the exception
}

// {
//   discriminator: "authorizedRequest",
//   auth: {
//     discriminator: loginWithoutChallenge,
//     persona: Persona
//   },
//   ongoingPersonaData: {
//     name: {
//       variant: 'western',
//       given: 'John',
//       family: 'Conner'
//     },
//     emailAddresses: ['jc@resistance.ai'],
//     phoneNumbers: ['123123123']
//   }
// }

const value = result.value
oneTimePersonaData example
const result = await sdk.request({
  discriminator: 'unauthorizedRequest',
  oneTimePersonaData: {
    isRequestingName: true,
  },
})

if (result.isErr()) {
  // code to handle the exception
}

// {
//   discriminator: "unauthorizedRequest",
//   oneTimePersonaData: {
//     name: {
//       variant: 'eastern',
//       given: 'Jet',
//       family: 'Li'
//     }
//   }
// }

const value = result.value

πŸ—‘οΈ Reset

You can send a reset request to ask the user to provide new values for ongoing accounts and/or persona data.

Request

type ResetRequestItem = {
  accounts: boolean
  personaData: boolean
}

Response

A Reset request has no response.

reset example
const result = await sdk.request({
  discriminator: 'authorizedRequest',
  auth: { discriminator: 'loginWithoutChallenge' },
  reset: { accounts: true, personaData: true },
})

if (result.isErr()) {
  // code to handle the exception
}

πŸ›‚ Auth

Sometimes your dApp may want a more personalized, consistent user experience and the Radix Wallet is able to login users with a Persona.

For a pure frontend dApp without any server backend, you may simply want to request such a login from the users's wallet so that the wallet keeps track of data sharing preferences for your dApp and they don't have to re-select that data each time they connect.

If your dApp does have a server backend and you are keeping track of users to personalize their experience, a Persona-based login provides strong proof of user identity, and the ID returned from the wallet provides a unique index for that user.

Once your dApp has a given identityAddress, it may be used for future requests for data that the user has given "ongoing" permission to share.

type Persona = {
  identityAddress: string
  label: string
}

Login

This request type results in the Radix Wallet asking the user to select a Persona to login to this dApp (or suggest one already used in the past there), and providing cryptographic proof of control.

// Hex encoded 32 random bytes
type Challenge = string

This proof comes in the form of a signed "challenge" against an on-ledger Identity component. For each Persona a user creates in the Radix Wallet, the wallet automatically creates an associated on-ledger Identity (which contains none of the personal data held in the wallet). This Identity includes a public key in its metadata, and the signature on the challenge uses the corresponding private key. ROLA (Radix Off-Ledger Authentication) may be used in your dApp backend to check if the login challenge is correct against on-ledger state.

type Proof = {
  publicKey: string
  signature: string
  curve: 'curve25519' | 'secp256k1'
}

The on-ledger address of this Identity will be the identityAddress used to identify that user – in future queries, or perhaps in your dApp's own user database.

If you are building a pure frontend dApp where the login is for pure user convenience, you may safely ignore the challenge and simply keep track of the identityAddress in the user's session for use in data requests that require it.

usePersona

If you have already identified the user via a login (perhaps for a given active session), you may specify a identityAddress directly without requesting a login from the wallet.

login example
const result = await sdk.request({
  discriminator: 'authorizedRequest',
  auth: { discriminator: 'loginWithoutChallenge' },
})

if (result.isErr()) {
  // code to handle the exception
}

// {
//   discriminator: "authorizedRequest",
//   auth: {
//     discriminator: 'loginWithoutChallenge',
//     persona: Persona
//   },
// }
const value = result.value
login with challenge example
// hex encoded 32 random bytes
const challenge = [...crypto.getRandomValues(new Uint8Array(32))]
  .map((item) => item.toString(16).padStart(2, '0'))
  .join('')

const result = await sdk.request({
  discriminator: 'authorizedRequest',
  auth: {
    discriminator: 'loginWithChallenge',
    challenge,
  },
})

if (result.isErr()) {
  // code to handle the exception
}

// {
//   discriminator: "authorizedRequest",
//   auth: {
//     discriminator: 'loginWithChallenge',
//     persona: Persona,
//     challenge: Challenge,
//     proof: Proof
//   },
// }
const value = result.value
usePersona example
const result = await sdk.request({
  discriminator: 'authorizedRequest',
  auth: {
    discriminator: 'usePersona',
    identityAddress:
      'identity_tdx_c_1p35qpky5sczp5t4qkhzecz3nm8tcvy4mz4997mqtuzlsvfvrwm',
  },
})

if (result.isErr()) {
  // code to handle the exception
}

// {
//   discriminator: "authorizedRequest",
//   auth: {
//     discriminator: usePersona,
//     persona: Persona
//   },
// }
const value = result.value

Request

type AuthRequestItem = AuthUsePersonaRequestItem | AuthLoginRequestItem
type AuthUsePersonaRequestItem = {
  discriminator: 'usePersona'
  identityAddress: string
}
type AuthLoginRequestItem =
  | AuthLoginWithoutChallengeRequestItem
  | AuthLoginWithChallengeRequestItem
type AuthLoginWithoutChallengeRequestItem = {
  discriminator: 'loginWithoutChallenge'
}
type AuthLoginWithChallengeRequestItem = {
  discriminator: 'loginWithChallenge'
  challenge: Challenge
}

Response

type AuthRequestResponseItem =
  | AuthUsePersonaRequestResponseItem
  | AuthLoginRequestResponseItem
type AuthUsePersonaRequestResponseItem = {
  discriminator: 'usePersona'
  persona: Persona
}
type AuthLoginRequestResponseItem =
  | AuthLoginWithoutChallengeResponseRequestItem
  | AuthLoginWithChallengeRequestResponseItem
type AuthLoginWithoutChallengeRequestResponseItem = {
  discriminator: 'loginWithoutChallenge'
  persona: Persona
}
type AuthLoginWithChallengeRequestResponseItem = {
  discriminator: 'loginWithChallenge'
  persona: Persona
  challenge: Challenge
  proof: Proof
}

πŸ’Έ Send transaction

Your dApp can send transactions to the user's Radix Wallet for them to review, sign, and submit them to the Radix Network.

Radix transactions are built using "transaction manifests", that use a simple syntax to describe desired behavior. See documentation on transaction manifest commands here.

It is important to note that what your dApp sends to the Radix Wallet is actually a "transaction manifest stub". It is completed before submission by the Radix Wallet. For example, the Radix Wallet will automatically add a command to lock the necessary amount of network fees from one of the user's accounts. It may also add "assert" commands to the manifest according to user desires for expected returns.

NOTE: Information will be provided soon on a "comforming" transaction manifest stub format that ensures clear presentation and handling in the Radix Wallet.

Build transaction manifest

We recommend using template strings for constructing simpler transaction manifests. If your dApp is sending complex manifests a manifest builder can be found in TypeScript Radix Engine Toolkit

sendTransaction

This sends the transaction manifest stub to a user's Radix Wallet, where it will be completed, presented to the user for review, signed as required, and submitted to the Radix network to be processed.

type SendTransactionInput = {
  transactionManifest: string
  version: number
  blobs?: string[]
  message?: string
}
  • requires transactionManifest - specify the transaction manifest
  • requires version - specify the version of the transaction manifest
  • optional blobs - used for deploying packages
  • optional message - message to be included in the transaction
sendTransaction example
const result = await sdk.sendTransaction({
  version: 1,
  transactionManifest: '...',
})

if (result.isErr()) {
  // code to handle the exception
}

const transactionIntentHash = result.value.transactionIntentHash

Errors

Error type Description Message
rejectedByUser User has rejected the request in the wallet
missingExtension Connector extension is not detected
canceledByUser User has canceled the request
walletRequestValidation SDK has constructed an invalid request
walletResponseValidation Wallet sent an invalid response
wrongNetwork Wallet is currently using a network with a network ID that does not match the one specified in request from Dapp (inside metadata) "Wallet is using network ID: (currentNetworkID), request sent specified network ID: (requestFromP2P.requestFromDapp.metadata.networkId)."
failedToPrepareTransaction Failed to get Epoch for Transaction Header
failedToCompileTransaction Failed to compile TransactionIntent or any other later form to SBOR using EngineToolkit
failedToSignTransaction Failed to sign any form of the transaction either with keys for accounts or with notary key, or failed to convert the signature to by EngineToolkit require format
failedToSubmitTransaction App failed to submit the transaction to Gateway for some reason
failedToPollSubmittedTransaction App managed to submit transaction but got error while polling it "TXID: <TXID_STRING>"
submittedTransactionWasDuplicate App submitted a transaction and got informed by Gateway it was duplicated "TXID: <TXID_STRING>"
submittedTransactionHasFailedTransactionStatus App submitted a transaction to Gateway and polled transaction status telling app it was a failed transaction "TXID: <TXID_STRING>"
submittedTransactionHasRejectedTransactionStatus App submitted a transaction to Gateway and polled transaction status telling app it was a rejected transaction "TXID: <TXID_STRING>"

License

The Wallet SDK binaries are licensed under the Radix Software EULA.

The Wallet SDK code is released under Apache 2.0 license.

  Copyright 2023 Radix Publishing Ltd

  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.

  You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

  See the License for the specific language governing permissions and limitations under the License.