also install metamask flask (until offical launch)
Run💡
The metastellar.io team manages and maintains the stellar wallet plugin for metamask. If implemented correctly, the end user should be aware they are using the stellar chain, but the experence should never feel like they are using a 'plug-in' hince the term snap.
The metastellar snap is a piece of code that lives inside the metamask wallet, and is automatically installed when requested by a web app. Connecting to the Stellar network using the snap is covered in ✨connect and install portion of the docs.
After the user installs the snap, a stellar wallet automatically created for them.
This wallet can be accessed, using the standard metamask rpc api. This means that if you have experence developing with metamask in ethereum this shouldn't be too different. (sadly, no web3.js stellar3.js yet 🤞).
As a developer basic idea, is you shouldn't have to focus on OUR wallet, you should focus on YOUR app. Ideally the flow would be.
[connect Metamask] -> [create Stellar TXN] -> [call signTxn] -> [submit signed txn] ✅
- There is NO npm package required!
- The only thing required is that the users computer has metamask flask
(just normal metamask after launch) - install flask
The wallet_requestSnaps method is used to connect to MetaMask and installs the Stellar Wallet if it's not already installed. This also generates the user's wallet.
/* //request connection */
async function connect(){
const connected = await window.ethereum.request({
method: 'wallet_requestSnaps',
params: {
[`npm:stellar-snap`]: {}
},
});
}
exec connect()
After the snap is connected the wallet_invokeSnap method is used to call Stellar Methods
//invoke a stellar method without callMetaStellar()
const request = {
method: 'wallet_invokeSnap',
params: {snapId:`npm:stellar-snap`,
request:{
method: `${'getDataPacket'}`
}
}
}
let address = (await window.ethereum.request(request)).currentAddress;
// retreives the stellar walletData
const walletData = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {snapId:`npm:stellar-snap`, request:{
method: `getDataPacket`,
}}
})
retreve standard wallet INFO! getDataPacket method
by default all methods are treated as mainnet, but any method can be issued to the testnet by using the testnet param. The testnet parameter can be used with all methods. If it dosn't make since for a certain method it is simply ignored silently.
example:
const result = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {snapId:`npm:stellar-snap`, request:{
method: `getBalance`,
params:{
testnet: true
}
}}
})
use with the Stellar-js-sdk
//invoke a stellar method with callMetaStellar
let stellarTransactionXDR:string = endTransaction.build().toXDR();
let signedTxnXDR:Promise<string> = callMetaStellar('signTransaction', {transaction : stellarTransactionXDR, testnet:true});
The easiest way to interact with the wallet is by coping the metastellar function
async function callMetaStellar(method, params){
//You Can Delete this section after offical launch
const isFlask = (
await window.ethereum?.request({ method: "web3_clientVersion" })
)?.includes("flask");
if(!isFlask){
alert("install Metamask Flask")
}
// ------------------------------------------------
if(method === 'connect'){
//This will also install stellar if the user has metamask
return await window.ethereum.request({
method: 'wallet_requestSnaps',
params: {
['npm:stellar-snap']: {}
},
});
}
const rpcPacket = {
method: 'wallet_invokeSnap',
params:{
snapId:'npm:stellar-snap',
request: {'method':method, params:params}
}
}
return await window.ethereum.request(rpcPacket);
}
invoke the callMetaStellar function
//connect:
const connected:boolean = await callMetaStellar('connect');
//getAddress:
const address:string = await callMetaStellar('getAddress');
//signTransaction:
interface signTxnParams{
transaction:string //StellarSDK.TransactionXDR as String)
testnet:boolean
}
let params = {transaction:txn, testnet:true}
const signedTxn:string = await callMetaStellar('signTransaction', params)
//returns a signed stellar transaction in XDR as a string
returns the accounts address as a string
const address = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {snapId:`npm:stellar-snap`, request:{
method: `getAddress`,
}}
})
grabs infomation related to the account requires account to be funded
const info = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {snapId:`npm:stellar-snap`, request:{
method: `getAccountInfo`,
params:{
testnet?: true | false
}
}}
})
getAccountInfo
gets the XLM balance of a wallet, returns 0 in unfunded wallets
const balance = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {snapId:`npm:stellar-snap`, request:{
method: `getBalance`,
params:{
testnet?: true | false
}
}}
})
getBalance
this method is used to transfer xlm and requires a funded account. after being called the wallet will generate a transaction, then prompt a user to accept if the user accepts the transaction it will be signed and broadcast to the network. will return transaction infomation. And send a notification stating whether the transaction was successful.
returns: StellarSDK.TransactionResult
const transactionInfomation = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {snapId:`npm:stellar-snap`, request:{
method: `getBalance`,
params:{
to: 'stellarAddress' //string
amount: '1000.45' //string represention of amount xlm to send
testnet?: true | false
}
}}
})
send xlm
this method funds the users wallet on the testnet,
const success = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {snapId:`npm:stellar-snap`,
request:{
method: 'fund'
}
}
})
This method signs an Arbitary Transaction
async function signTransaction(){
const transaction = new StellarSdk.TransactionBuilder(account, { fee, networkPassphrase: "Test SDF Network ; September 2015" });
// Add a payment operation to the transaction
console.log("transaction builder initilazed");
await transaction.addOperation(StellarSdk.Operation.payment({
destination: receiverPublicKey,
// The term native asset refers to lumens
asset: StellarSdk.Asset.native(),
// Specify 350.1234567 lumens. Lumens are divisible to seven digits past
// the decimal. They are represented in JS Stellar SDK in string format
// to avoid errors from the use of the JavaScript Number data structure.
amount: '350.1234567',
}));
console.log("operations added")
// Make this transaction valid for the next 30 seconds only
await transaction.setTimeout(30);
console.log("timeout set");
// Uncomment to add a memo (https://www.stellar.org/developers/learn/concepts/transactions.html)
// .addMemo(StellarSdk.Memo.text('Hello world!'))
const endTransaction = await transaction.build();
const xdrTransaction = endTransaction.toXDR();
console.log(xdrTransaction);
const response = await window.ethereum.request({
method: 'wallet_invokeSnap',
params:{snapId:snapId, request:{
method: 'signTransaction',
params:{
transaction: xdrTransaction,
testnet: testnet
}
}}
})
console.log(response);
}
if(!wallet_funded){
await Screens.RequiresFundedWallet(request.method, wallet.address);
}
return await operations.signAndSubmitTransaction(params.transaction);
retreves wallet info about the user, including names, addressess, and balances
returns DataPacket
const walletInfo: DataPacket = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {`npm:stellar-snap`, request:{
method: `getDataPacket`,
}}
})
get data packet
changes the connected account
interface setCurrentAccountParams :{
address:string
}
const switchAccountParams:setCurrentAccountParams = {
address:`${WalletAddress}`
}
const result = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {`npm:stellar-snap`,
request:{
method: switchAccountParams,
params
}
}
})
displays the stellar address and a qr code in the extension
returns: boolean
const result = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {`npm:stellar-snap`,
request:{
method: `showAddress`,
}
}
})
Show Address
creates a new Account on the wallet
parameters: {name:string}
returns: bool
interface createAccountParams{
name: string
}
const createAccountResult = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {`npm:stellar-snap`,
request: {
method: `createAccount`,
params: {
name: `${"Account-name"}`
}
}}
})
CreateAccount
returns a list of all stellar accounts in the wallet
const accountList = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {`npm:stellar-snap`,
request: {
method: `listAccounts`,
}}
})
selects an account by address and changes its name
const result = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {`npm:stellar-snap`,
request:{
method: `renameAccount`,
params:{
address: `${accountAddress}`,
name: `${"New-Account-Name"}`
}
}}
})
opens a dialog where the user is prompted to import their private key, if they choose
throws on error
const success:boolean = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {
snapId:`npm:stellar-snap`,
request:{
method: "importAccount",
}}
})
gets all assets for the current Wallet
returns walletAsset[]
const assets: walletAsset[] = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {
snapId:`npm:stellar-snap`,
request:{
method: `getAssets`,
params:{
testnet: true
}
}}
})
sendAuthRequest is used to sign-in with
const result = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {`npm:stellar-snap`, request:{
method: `sendAuthRequest`,
params:{
url: `${endpoint}`,
data: Object(postData),
challenge: `${toBeSigned}`
}
}}
})
const auth = new Auth(wallet.keyPair);
return await auth.signData(params.challenge);
return await Screens.revealPrivateKey(wallet);
// -------------------------------- Methods That Require a funded Account ------------------------------------------
if(!wallet_funded){
await Screens.RequiresFundedWallet(request.method, wallet.address);
throw new Error('Method Requires Account to be funded');
}
return await operations.transferAsset(params.to, params.amount, params.asset);
return await Screens.setUpFedAccount(wallet);
The Wallet also supports sorroban, To sign a SorobanCall futurenet must be set to true on the params object.
async function callContract() {
console.log("here in callContract");
const sourcePublicKey = await window.ethereum.request({
method: 'wallet_invokeSnap',
params: {snapId:snapId, request:{
method: 'getAddress',
}}
})
const server = new SorobanClient.Server('https://rpc-futurenet.stellar.org');
console.log("getting account")
const account = await server.getAccount(sourcePublicKey);
console.log("account is: ")
console.log(account);
console.log(SorobanClient);
const contract = new SorobanClient.Contract("CCNLUNUY66TU4MB6JK4Y4EHVQTAO6KDWXDUSASQD2BBURMQT22H2CQU7")
console.log(contract)
const arg = SorobanClient.nativeToScVal("world")
console.log("arg is: ")
console.log(arg)
let call_operation = contract.call('hello', arg);
console.log(call_operation)
let transaction = new SorobanClient.TransactionBuilder(account, { fee: "150", networkPassphrase: SorobanClient.Networks.FUTURENET })
.addOperation(call_operation) // <- funds and creates destinationA
.setTimeout(30)
.build();
console.log(transaction)
const preparedTransaction = await server.prepareTransaction(transaction, SorobanClient.Networks.FUTURENET);
console.log("prepairedTxn: ");
console.log(preparedTransaction);
const tx_XDR = preparedTransaction.toXDR();
const signedXDR = await window.ethereum.request(
{method: 'wallet_invokeSnap',
params: {
snapId:snapId,
request:{
method: 'signTransaction',
params:{
transaction: tx_XDR,
futurenet: true
}
}
}
}
)
console.log(signedXDR)
try{
const transactionResult = await server.sendTransaction(signedXDR);
console.log(JSON.stringify(transactionResult, null, 2));
console.log('\nSuccess! View the transaction at: ');
console.log(transactionResult)
} catch (e) {
console.log('An error has occured:');
console.log(e);
}
}
An interface that contains infomation about the wallet
export interface DataPacket{
name: string, //comment
currentAddress: string,
mainnetAssets?: walletAsset[],
testnetAssets?: walletAsset[],
accounts: Array<{name:String, address:String}>
mainnetXLMBalance: string,
testnetXLMBalance: string,
fedName: string | null
}
a type that represents a the balance of asset held by a wallet
export type walletAsset = AssetBalance | NativeBalance
export interface NativeBalance {
balance:string,
liquidity_pool_id?:string,
limit: string,
buying_liabilites: string,
selling_liabilites: string,
sponser?: string,
last_modified_ledger: number,
is_authorized: boolean,
is_authorized_to_maintain_liabilites: boolean,
is_clawback_enabled: boolean,
asset_type: "native",
asset_issuer: "native"
asset_code: "XLM"
}
export interface AssetBalance {
balance: string, //number
liquidity_pool_id?: string, //number
limit: string, //number
buying_liabilites: string, //number
selling_liabilites: string, //number
sponser?: string, //address
last_modified_ledger: number,
is_authorized: boolean,
is_authorized_to_maintain_liabilites: boolean,
is_clawback_enabled: boolean,
asset_type: "credit_alphanum4"|"credit_alphanum12"
asset_code: string,
asset_issuer: string, //address
}
foo@bar:~$ yarn
...
foo@bar:~$ npx mm-snap build
...
Build success: 'src\index.ts' bundled as 'dist\bundle.js'!
Eval Success: evaluated 'dist\bundle.js' in SES!
foo@bar:npx mm-snap serve
Starting server...
Server listening on: http://localhost:8080
and just like that you should be good to go.
keys are generated on the fly, anytime a method is invoked. This works by requesting private entropy from the metamask wallet inside of the snaps secure execution enviroment, and using that entropy to generate a users keys. This entropy is static, and based on the users ethereum account. This means that we at no point store keys, and the fissile material is handled by metamask.
Because keys are handled in this way, when a user recovers their metamask account, they will also recover their stellar account, which means that there isn't another mnemonic to save.