Onboard Users
Learn how to map user accounts to blockchain accounts with USD and NGN tokens.
// npm install --save stellar-sdk simple-sha256
var StellarSdk = require('stellar-sdk');
const sha256 = require('simple-sha256')
const crypto = require("crypto")
// In this tutorial, we are going to map a normal account (e.g. user_abc), to blockchain account (e.g. GCSJ..) with access to its secret KeyboardEvent.DOM_KEY_LOCATION_LEFT
// We will use a mnemonic to help us to the mapping without depending on a database.
// The idea is to append the username to the mnemonic, generate a hash, and use that hash to get a private seed we can use to generate a blockchain account.
// 1. Get an app mnemonic.
// 2. Get a private seed from hash(mnemonic + user_account)
// 3. Use the private key in 2 to generate a new private_key, public_key pair for a new blockchain account.
// function mapping_username_to_blockchain_account
// Note that the same username will always produce the same (public_key, secret_key) pair. So you don't need to store the secret key or public key in any DB.
function mapping_username_to_blockchain_account(username) {
// The app mnemonic seed we will use throughout. This should be kept server side and never exposed to end users.
// You can generate your own mnemonics with the help of the following tool: https://iancoleman.io/bip39/#english
// We recommend at least 24 words for the app mnemonic.
// In prod, use environment variables to store this app mnemonic. Do not keep sensitive keys in code.
const APP_MNEMONIC = "nasty school dial promote abstract defy damp minor lyrics carry unit aware special render subject fabric stand latin diesel once crumble hidden ancient exclude"
// Create a blockchain keypair from a hash of the mnemonic plus username. The SDK expects a 32 byte buffer to be used as seed.
// we hash with sha256.
const salt = username + "NS;X=VB&=cD75AEg>kr" ; //this salt shouldn't change. but can be placed in an environment variable.
let hash = sha256.sync(username + APP_MNEMONIC)
//we could have stopped at using only sha256, but let's make it harder for anyone else trying to bruteforce by adding an scrypt hash in the mix.
// scrypt hashes are 'slower' to generate than sha256 alone, which makes them more than ideal for our scenario.
let buffer = crypto.scryptSync(hash, salt, 32 , { N: Math.pow(2, 14) , r: 8, p: 1});
return StellarSdk.Keypair.fromRawEd25519Seed(buffer)
}
function create_account_txn(sourceAccount, sourceAccountKeyPair, keypair) {
// ISSUER_PUBLIC_KEY represents the public key of the issuer account. An issuer account usually represents a financial institution.
// for the sake of this tutorial, note that the secret key for the issuer below is SA6R4KLE5MD4EKQYSNGLXBGQMAKHEPWZGRERA75H2CPO2KZ4627QQZGP.
// do not store secret keys in code.
// make sure this issuer account is funded.
const ISSUER_PUBLIC_KEY = "GD6W7MMLY3EVMWSYSM6ZIWXWBKHSLUEL6UZOMLXYRMEE3LLUTZHQVR6B"
transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: "Bantu Testnet",
})
.addOperation(
StellarSdk.Operation.createAccount({
destination: keypair.publicKey(),
startingBalance: "10",
}),
)
.addOperation(
StellarSdk.Operation.changeTrust({
asset: new StellarSdk.Asset("NGN", ISSUER_PUBLIC_KEY),
source: keypair.publicKey()
}),
)
.addOperation(
StellarSdk.Operation.changeTrust({
asset: new StellarSdk.Asset("USD", ISSUER_PUBLIC_KEY),
source: keypair.publicKey()
}),
)
// Wait a maximum of three minutes for the transaction
.setTimeout(180)
.build();
transaction.sign(sourceAccountKeyPair)
transaction.sign(keypair)
return transaction;
}
// function onboard_blockchain_account_if_necessary
// funds a blockchain account, and adds trustlines for USD and NGN.
function onboard_blockchain_account_if_necessary(keypair) {
// Here's the plan.
// If the blockchain account is already funded and has the necessary USD and NGN trustlines, we do nothing.
// If the blockchain account is not funded, we will fund it using native blockchain tokens from another FAUCET account, and add the necessary trustlines.
// Do NOT USE THIS CODE in production. It cannot handle heavy load. To handle heavy load, we will need to add Channel accounts. We will tackle channel accounts in another tutorial.
// Also, for production use, please do not store secret keys in the code.... use environment variables.
// FAUCET_ACCOUNT_SECRET_KEY is the faucet account secret key that will be used to fund other unfunded blockchain accounts.
// Make sure the faucet account is funded.
// In prod, use environment variables to store secret keys. Do not keep secret keys in code.
const FAUCET_ACCOUNT_SECRET_KEY = "SC2W73J2UT4V2TJR43EL2B2T3RJFZQUYXY7ENABPS75MRALCGS6XPZ4F"
// The public key of the faucet account above is GAVGVTAV7NF7WMQUDK3CHTWF3YIAOMVO4FNYJSYWE3GMYSSCMEHNPLQT
let faucetKeyPair = StellarSdk.Keypair.fromSecret(FAUCET_ACCOUNT_SECRET_KEY)
//now let's check if our blockchain account exists in the blockchain.
var server = new StellarSdk.Server("https://expansion-testnet.bantu.network");
server
.loadAccount(faucetKeyPair.publicKey())
// If the account is not found, surface a nicer error message for logging.
.catch(function (error) {
if (error instanceof StellarSdk.NotFoundError) {
throw new Error("The faucet account " + faucetKeyPair.publicKey() + " does not exist!");
} else return error;
})
.then(function (faucetAccount) {
server.loadAccount(keypair.publicKey())
.then(function (destinationAccount) {
//destination account already exists. Do nothing else.
console.log("Destination account already exists. Go check the account details of %s at %s/%s", keypair.publicKey(), "https://explorer.Bantu.stargate.is/account", keypair.publicKey())
throw new Error("Destination account " + keypair.publicKey() + " already exists");
})
.catch(function (error) {
if (!(error instanceof StellarSdk.NotFoundError)) {
//we are only interested in NotFoundErrors, which means the account doesn't exist yet on the blockchain.
throw error;
}
//keypair account does not exist. Create a create account txn and return.
let txn = create_account_txn(faucetAccount, faucetKeyPair, keypair)
console.log("Txn XDR is %s", txn.toXDR())
return server.submitTransaction(transaction);
})
.then(function (result) {
console.log("Success! Results:", result);
console.log("Go check the account details of %s at %s/%s", keypair.publicKey(), "https://explorer.Bantu.stargate.is/account", keypair.publicKey())
})
.catch(function (error) {
console.error("Something went wrong!", error);
// If the result is unknown (no response body, timeout etc.) we simply resubmit
// already built transaction:
// server.submitTransaction(transaction);
});
})
}
let username = "segun"; // change this username to whatever you want in this tutorial.
let userKeyPair = mapping_username_to_blockchain_account(username)
//note that the same username will always produce the same (public_key, secret_key) pair. So you don't need to store the secret key or public key in any DB.
console.log("User %s, has public_key = %s and secret_key = %s", username, userKeyPair.publicKey(), userKeyPair.secret())
onboard_blockchain_account_if_necessary(userKeyPair)
using System;
using System.Security.Cryptography;
using System.Text;
using stellar_dotnet_sdk; //Install-Package stellar-dotnet-sdk -Version 7.1.4
using BCrypt.Net;
using System.Net;
using System.Threading.Tasks;
namespace ConsoleApp5
{
class Program
{
// function mapping_username_to_blockchain_account
// Note that the same username will always produce the same (public_key, secret_key) pair. So you don't need to store the secret key or public key in any DB.
static KeyPair MapUsernameToBlockchainAccount(String username)
{
// The app mnemonic seed we will use throughout. This should be kept server side and never exposed to end users.
// You can generate your own mnemonics with the help of the following tool: https://iancoleman.io/bip39/#english
// We recommend at least 24 words for the app mnemonic.
// In prod, use environment variables to store this app mnemonic. Do not keep sensitive keys in code.
String APP_MNEMONIC = "nasty school dial promote abstract defy damp minor lyrics carry unit aware special render subject fabric stand latin diesel once crumble hidden ancient exclude";
// Create a blockchain keypair from a hash of the mnemonic plus username. The SDK expects a 32 byte buffer to be used as seed.
// we are using two hashes here... first we hash with sha256, then we hash with bcrypt.
var crypt = new SHA256Managed();
string hash = String.Empty;
byte[] crypto = crypt.ComputeHash(Encoding.UTF8.GetBytes(APP_MNEMONIC + username));
byte[] seed = new byte[32];
Buffer.BlockCopy(crypto, 0, seed, 0, 32);
return KeyPair.FromSecretSeed(seed);
}
static Transaction CreateAccountTxn(stellar_dotnet_sdk.responses.AccountResponse sourceAccount, KeyPair sourceAccountKeyPair, KeyPair keypair)
{
// ISSUER_PUBLIC_KEY represents the public key of the issuer account. An issuer account usually represents a financial institution.
// for the sake of this tutorial, note that the secret key for the issuer below is SA6R4KLE5MD4EKQYSNGLXBGQMAKHEPWZGRERA75H2CPO2KZ4627QQZGP.
// do not store secret keys in code.
// make sure this issuer account is funded.
String ISSUER_PUBLIC_KEY = "GD6W7MMLY3EVMWSYSM6ZIWXWBKHSLUEL6UZOMLXYRMEE3LLUTZHQVR6B";
Network network = new Network("Bantu Testnet");
Network.Use(network);
var transaction = new TransactionBuilder(sourceAccount)
.AddOperation(new CreateAccountOperation.Builder(keypair, "10").Build())
.AddOperation(new ChangeTrustOperation.Builder(Asset.CreateNonNativeAsset("NGN", ISSUER_PUBLIC_KEY)).SetSourceAccount(keypair).Build())
.AddOperation(new ChangeTrustOperation.Builder(Asset.CreateNonNativeAsset("USD", ISSUER_PUBLIC_KEY)).SetSourceAccount(keypair).Build())
.AddOperation(new ManageDataOperation.Builder("status", Encoding.UTF8.GetBytes("onboarded")).Build())
.Build();
transaction.Sign(sourceAccountKeyPair);
transaction.Sign(keypair);
return transaction;
}
static async Task OnboardBlockchainAccountIfNecessary(KeyPair keypair)
{
// Here's the plan.
// If the blockchain account is already funded and has the necessary USD and NGN trustlines, we do nothing.
// If the blockchain account is not funded, we will fund it using native blockchain tokens from another FAUCET account, and add the necessary trustlines.
// Do NOT USE THIS CODE in production. It cannot handle heavy load. To handle heavy load, we will need to add Channel accounts. We will tackle channel accounts in another tutorial.
// Also, for production use, please do not store secret keys in the code.... use environment variables.
// FAUCET_ACCOUNT_SECRET_KEY is the faucet account secret key that will be used to fund other unfunded blockchain accounts.
// Make sure the faucet account is funded.
// In prod, use environment variables to store secret keys. Do not keep secret keys in code.
String FAUCET_ACCOUNT_SECRET_KEY = "SC2W73J2UT4V2TJR43EL2B2T3RJFZQUYXY7ENABPS75MRALCGS6XPZ4F";
// The public key of the faucet account above is GAVGVTAV7NF7WMQUDK3CHTWF3YIAOMVO4FNYJSYWE3GMYSSCMEHNPLQT
KeyPair faucetKeyPair = KeyPair.FromSecretSeed(FAUCET_ACCOUNT_SECRET_KEY);
var server = new Server("https://expansion-testnet.bantu.network");
stellar_dotnet_sdk.responses.AccountResponse faucetAccount = null;
try
{
faucetAccount = await server.Accounts.Account(faucetKeyPair.AccountId);
}
catch(stellar_dotnet_sdk.requests.HttpResponseException ex)
{
if(ex.StatusCode == ((int)HttpStatusCode.NotFound))
{
Console.WriteLine("[ERROR] Faucet account {0} is unfunded.", faucetKeyPair.AccountId);
return;
}
throw ex;
}
stellar_dotnet_sdk.responses.AccountResponse keyPairAccount = null;
try
{
keyPairAccount = await server.Accounts.Account(keypair.AccountId);
//no exception. means already funded. Return.
return;
}
catch (stellar_dotnet_sdk.requests.HttpResponseException ex)
{
if (ex.StatusCode == ((int)HttpStatusCode.NotFound))
{
//account is unfunded... we can proceed.
}
else
{
//unknown error.
throw ex;
}
}
var txn = CreateAccountTxn(faucetAccount, faucetKeyPair, keypair);
var result = await server.SubmitTransaction(txn);
if (result.IsSuccess())
{
return;
}
else
{
Console.WriteLine("ERROR submitting txn {0} ; {1}", result.EnvelopeXdr, result.ResultXdr);
}
}
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
String username = "segun"; // change this username to whatever you want in this tutorial.
var userKeyPair = MapUsernameToBlockchainAccount(username);
//note that the same username will always produce the same (public_key, secret_key) pair. So you don't need to store the secret key or public key in any DB.
Console.WriteLine("User {0}, has public_key = {1} and secret_key = {2}", username, userKeyPair.Address, userKeyPair.SecretSeed);
OnboardBlockchainAccountIfNecessary(userKeyPair).Wait();
}
}
}
import org.stellar.sdk.*;
import org.stellar.sdk.requests.ErrorResponse;
import org.stellar.sdk.responses.AccountResponse;
import org.stellar.sdk.responses.SubmitTransactionResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Main {
// The app mnemonic seed we will use throughout. This should be kept server side and never exposed to end users.
// You can generate your own mnemonics with the help of the following tool: https://iancoleman.io/bip39/#english
// We recommend at least 24 words for the app mnemonic.
// In prod, use environment variables to store this app mnemonic. Do not keep sensitive keys in code.
final static String APP_MNEMONIC = "nasty school dial promote abstract defy damp minor lyrics carry unit aware special render subject fabric stand latin diesel once crumble hidden ancient exclude";
final static String SALT = "NS;X=VB&=cD75AEg>kr";
//Maximum trustline limit below.
final static String LIMIT = "922337203685.4775807";
static KeyPair mapUsernameToKeyPair(String username) throws NoSuchAlgorithmException {
String toDigest = username + APP_MNEMONIC + SALT;
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(toDigest.getBytes(StandardCharsets.UTF_8));
//you can add additional hashing algorithms like scrypt or bcrypt to make your mapping more resilient to brute-force attempts.
//secret seeds require only 32 bytes
return KeyPair.fromSecretSeed(Arrays.copyOfRange(encodedhash, 0, 32));
}
static boolean isKeyPairFunded(KeyPair keyPair) throws IOException {
Server server = new Server("https://expansion-testnet.bantu.network");
try {
AccountResponse accountResponse = server.accounts().account(keyPair.getAccountId());
//no error was thrown... which means account already exists.
return true;
} catch (ErrorResponse errorResponse) {
if (errorResponse.getCode() == 404) {
//account does not exist..
return false;
} else {
System.out.println(errorResponse.getCode());
throw errorResponse;
}
}
}
static void onboardKeyPair(KeyPair keyPair) throws IOException, AccountRequiresMemoException {
Server server = new Server("https://expansion-testnet.bantu.network");
final String FAUCET_ACCOUNT_SECRET_KEY = "SBXIBKL3FGP4I74X4HYLXFVCHU6C4TVM3FUTDMXV7R4WOYW32M6AYJHQ";
KeyPair faucetKeyPair = KeyPair.fromSecretSeed(FAUCET_ACCOUNT_SECRET_KEY);
if (!isKeyPairFunded(faucetKeyPair)) {
System.out.println("faucet keypair is not funded. Aborting.");
System.out.println(faucetKeyPair.getAccountId());
return;
}
//the secret key for the following issuer account is: SD2WBQNZQRQJMGBXUZUKJYNLJOMS4DNI5X3AB5EDJXKIKKMEV3GZGESL
//but you only need it's public key.
final String ISSUER_PUBLIC_KEY = "GBOOHPZ7R2KYYACA23GSANG2EJ5ZXHE3JBSUQOILMNVQFLEDWZJTP3G5";
KeyPair issuerKeyPair = KeyPair.fromAccountId(ISSUER_PUBLIC_KEY);
if (!isKeyPairFunded(issuerKeyPair)) {
System.out.println("issuer keypair is not funded. Aborting.");
System.out.println(issuerKeyPair.getAccountId());
return;
}
Network network = new Network("Bantu Testnet");
AccountResponse faucetAccountResponse = server.accounts().account(faucetKeyPair.getAccountId());
Transaction.Builder transactionBuilder = new Transaction.Builder(faucetAccountResponse, network);
boolean keyPairAlreadyFunded = isKeyPairFunded(keyPair);
if (!keyPairAlreadyFunded) {
System.out.println("keypair has not been previously funded. Adding create account operation to fund it");
//create keyPair
CreateAccountOperation createAccountOperation = new CreateAccountOperation.Builder(keyPair.getAccountId(), "20").build();
transactionBuilder.addOperation(createAccountOperation);
}
{
//set email of keyPair
ManageDataOperation manageDataOperation = new ManageDataOperation.Builder("email", "bogus@me.com".getBytes(StandardCharsets.UTF_8)).setSourceAccount(keyPair.getAccountId()).build();
transactionBuilder.addOperation(manageDataOperation);
}
{
//set phonenumber of keyPair
ManageDataOperation manageDataOperation = new ManageDataOperation.Builder("phonenumber", "+234xxxxxxxxx".getBytes(StandardCharsets.UTF_8)).setSourceAccount(keyPair.getAccountId()).build();
transactionBuilder.addOperation(manageDataOperation);
}
{
// Add Naira trustline.
ChangeTrustOperation changeTrustOperation = new ChangeTrustOperation.Builder(Asset.createNonNativeAsset("NGN", ISSUER_PUBLIC_KEY), LIMIT).setSourceAccount(keyPair.getAccountId()).build();
transactionBuilder.addOperation(changeTrustOperation);
}
{
// Add USD trustline.
ChangeTrustOperation changeTrustOperation = new ChangeTrustOperation.Builder(Asset.createNonNativeAsset("USD", ISSUER_PUBLIC_KEY), LIMIT).setSourceAccount(keyPair.getAccountId()).build();
transactionBuilder.addOperation(changeTrustOperation);
}
transactionBuilder.setTimeout(Transaction.Builder.TIMEOUT_INFINITE);
//optional, let's add a memo.
transactionBuilder.addMemo(Memo.text("test-memo"));
transactionBuilder.setBaseFee(100);
//let's build the transaction, sign and submit to horizon.
Transaction transaction = transactionBuilder.build();
transaction.sign(faucetKeyPair);
transaction.sign(keyPair);
//optionally log the transaction to be submitted to the blockchain
System.out.println(transaction.toEnvelopeXdrBase64());
SubmitTransactionResponse response = server.submitTransaction(transaction);
if (response.isSuccess()) {
System.out.println("Done");
} else {
System.out.println(response.getResultXdr().get());
}
}
public static void main(String[] args) throws NoSuchAlgorithmException, IOException, AccountRequiresMemoException {
System.out.println("Hello, World!");
String user = "saint";
KeyPair keyPair = mapUsernameToKeyPair(user);
System.out.println("The keypair credentials for " + user + " are : ");
System.out.println("===");
System.out.println(new String(keyPair.getSecretSeed()));
System.out.println(keyPair.getAccountId());
System.out.println("===");
onboardKeyPair(keyPair);
}
}