How to save keypairs and send tokens in Solana, using node.js? - javascript

I am attempting at using Solana over Ethereum for a particular solution.
I am using NPM to target Solana's API's. The packages being used are #solana/web3.js and #solana/spl-token. I've been able to create a successful connection and generate keypairs. I would like to be able to save a generated keypair, but the problem I face is that it's encoded, and the second issue to highlight is when I use the .toString(); it decodes it just fine but that can't be used in an airdrop.
const solanaWeb3 = require("#solana/web3.js"),
const solanatoken = require("#solana/spl-token");
(async () => {
// Connect to cluster
var connection = new solanaWeb3.Connection(
solanaWeb3.clusterApiUrl("devnet"),
"confirmed"
);
// Generate a new wallet keypair and airdrop SOL
var wallet = solanaWeb3.Keypair.generate();
console.log("public key...", wallet.publicKey)
// The stringPK is an example of what wallet.publicKey returns. When taking wallet.publicKey and using .toString(); it returns the decoded publicKey but that can't be used in an airdrop.
const stringPK = `_bn: <BN: e8a4421b46f76fdeac76819ab21708cc4a4b9c9c7fc2a22e373443539cee1a81>`;
console.log("private key...", JSON.stringify(wallet.secretKey.toString()));
console.log(solanaWeb3.PublicKey.decode(wallet.publicKey));
var airdropSignature = await connection.requestAirdrop(
stringPK,
solanaWeb3.LAMPORTS_PER_SOL
);
//wait for airdrop confirmation
await connection.confirmTransaction(airdropSignature);
let account = await connection.getAccountInfo(wallet.publicKey);
console.log(account);
})();

To save the generated keypair in JS, your best bet is to use secretKey on the Keypair object [1], write that to a file. To load it back up, you'll read that file and then use fromSecretKey to get back your Keypair [2]
As for your other issue, requestAirdrop takes a PublicKey, so instead, you should just do:
var airdropSignature = await connection.requestAirdrop(
wallet.publicKey,
solanaWeb3.LAMPORTS_PER_SOL
);
[1] https://solana-labs.github.io/solana-web3.js/classes/Keypair.html#secretKey
[2] https://solana-labs.github.io/solana-web3.js/classes/Keypair.html#fromSecretKey

Related

Sign a message with EdDSA algorithm in Javascript to get JWT

I need to get JWT with EdDSA algorithm to be able to use an API. I have the private key to sign the message and I could do that with PHP with the next library: https://github.com/firebase/php-jwt (you can see the example with EdDSA at README). Now I need to do the same in JS but I didn't find the way to get JWT with a given secret key (encoded base 64) like that (only an example is not the real secretKey):
const secretKey = Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==
I tried a lot of libraries like jose, js-nacl, crypto, libsodium, etc. And I am really close to get the JWT with libsodium library, now I attach the code:
const base64url = require("base64url");
const _sodium = require("libsodium-wrappers");
const moment = require("moment");
const getJWT = async () => {
await _sodium.ready;
const sodium = _sodium;
const privateKey =
"Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==";
const payload = {
iss: "test",
aud: "test.com",
iat: 1650101178,
exp: 1650101278,
sub: "12345678-1234-1234-1234-123456789123"
};
const { msg, keyAscii} = encode(payload, privateKey, "EdDSA");
const signature = sodium.crypto_sign_detached(msg, keyDecoded); //returns Uint8Array(64)
//Here is the problem.
};
const encode = (payload, key, alg) => {
const header = {
typ: "JWT",
alg //'EdDSA'
};
const headerBase64URL = base64url(JSON.stringify(header));
const payloadBase64URL = base64url(JSON.stringify(payload));
const headerAndPayloadBase64URL = `${headerBase64URL}.${payloadBase64URL}`;
const keyAscii= Buffer.from(key, "base64").toString("ascii");
return {headerAndPayloadBase64URL , keyAscii}
};
The problem is in the sodium.crypto_sign_detached function because it returns an Uint8Array(64) signature and and I need the JWT like that:
eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
How can I change the Uint8Array(64) to get the signature in a right format to get the JWT? I tried with base64, base64url, hex, text, ascii, etc and the final JWT is not valid (because the signature is wrong).
If you compare my code with the code that I mentioned with PHP is very similar but the function sodium.crypto_sign_detached returns Uint8Array(64) at JS library and the same function in PHP returns an string and I can get the token.
Or maybe there a way to adapt my given private key for use in other library (like crypto or jose where I received an error for the private key format)
Thank you!
In the posted NodeJS code there are the following issues:
crypto_sign_detached() returns the signature as a Uint8Array, which can be imported with Buffer.from() and converted to a Base64 string with base64url().
Concatenating headerAndPayloadBase64URL and the Base64url encoded signature with a . as separator gives the JWT you are looking for.
The raw private key must not be decoded with 'ascii', as this generally corrupts the data. Instead, it should simply be handled as buffer. Note: If for some reason a conversion to a string is required, use 'binary' as encoding, which produces a byte string (however, this is not an option with crypto_sign_detached() as this function expects a buffer).
With these changes, the following NodeJS code results:
const _sodium = require('libsodium-wrappers');
const base64url = require("base64url");
const getJWT = async () => {
await _sodium.ready;
const sodium = _sodium;
const privateKey = "Dm2xriMD6riJagld4WCA6zWqtuWh40UzT/ZKO0pZgtHATOt0pGw90jG8BQHCE3EOjiCkFR2/gaW6JWi+3nZp8A==";
const payload = {
iss: "test",
aud: "test.com",
iat: 1650101178,
exp: 1650101278,
sub: "12345678-1234-1234-1234-123456789123"
};
const {headerAndPayloadBase64URL, keyBuf} = encode(payload, privateKey, "EdDSA");
const signature = sodium.crypto_sign_detached(headerAndPayloadBase64URL, keyBuf);
const signatureBase64url = base64url(Buffer.from(signature));
console.log(`${headerAndPayloadBase64URL}.${signatureBase64url}`) // eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
};
const encode = (payload, key, alg) => {
const header = {
typ: "JWT",
alg //'EdDSA'
};
const headerBase64URL = base64url(JSON.stringify(header));
const payloadBase64URL = base64url(JSON.stringify(payload));
const headerAndPayloadBase64URL = `${headerBase64URL}.${payloadBase64URL}`;
const keyBuf = Buffer.from(key, "base64");
return {headerAndPayloadBase64URL, keyBuf};
};
getJWT();
Test:
Since Ed25519 is deterministic, the NodeJS code can be checked by comparing both JWTs: If, as in the above NodeJS code, the same header and payload are used as in the PHP code, the same signature and thus the same JWT is generated as by the PHP code, namely:
eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJ0ZXN0IiwiYXVkIjoidGVzdC5jb20iLCJpYXQiOjE2NTAxMDExNzgsImV4cCI6MTY1MDEwMTI3OCwic3ViIjoiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MTIzIn0.f7WG_02UKljrMeVVOTNNBAGxtLXJUT_8QAnujNhomV18Pn5cU-0lHRgVlmRttOlqI7Iol_fHut3C4AOXxDGnAQ
which shows that the NodeJS code works.
Note that instead of the moment package, Date.now() could be used. This will return the time in milliseconds, so the value has to be divided by 1000, e.g. Math.round(Date.now()/1000), but saves a dependency.

How to extract image URL from ens avatar text record

When using ethers JavaScript library to retrieve ENS text records from the Ethereum Name Service. I got the NFT URI from the text record but not the image URL. Please is there any way/service to convert this metadata URI to an HTTPS image URL.
TO RECREATE:
TERMINAL : npm install --save ethers
Code:
var ethers = require('ethers');
var url = 'https://mainnet.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161';
var provider = new ethers.providers.JsonRpcProvider(url);
async function main() {
const resolver = await provider.getResolver("brantly.eth");
const avatar = await resolver.getAvatar();
const avatarMetaData = await resolver.getText("avatar");
console.log(`Avatar Metadata: ${avatarMetaData}`);
}
main();
.
Output: Avatar Metadata: eip155:1/erc721:0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6/2430
The EIP155... Is not an imageURL.
I'd prefer it came in the form of {https://...}.
Is there any way to achieve this or convert the NFT URI EIP155... To an image or image URL.
As far as I know you will have to go through the entire process to get that IPFS URI.
You will want to take the contract address and token ID from that returned avatar string (the last 2 parts) and call the tokenURI ABI method, parse the JSON and read image there:
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contract = new ethers.Contract(address, abi, signer);
const uri = await contract.tokenURI(tokenId)
const data = (await axios.get(uri)).data;
const image = data.image;

How to modify my code to send custom SPL Token instead of regular SOL?

I'm building a website where people log in to their phantom wallet then by clicking on a button they will send a certain amount of our custom token to one wallet.
The code shown below is working with SOL and I would like to make it work with our custom SPL token, I have the mint address of the token but I couldn't find any way to make it work. Could anyone help me?
async function transferSOL(toSend) {
// Detecing and storing the phantom wallet of the user (creator in this case)
var provider = await getProvider();
console.log("Public key of the emitter: ",provider.publicKey.toString());
// Establishing connection
var connection = new web3.Connection(
"https://api.mainnet-beta.solana.com/"
);
// I have hardcoded my secondary wallet address here. You can take this address either from user input or your DB or wherever
var recieverWallet = new web3.PublicKey("address of the wallet recieving the custom SPL Token");
var transaction = new web3.Transaction().add(
web3.SystemProgram.transfer({
fromPubkey: provider.publicKey,
toPubkey: recieverWallet,
lamports: (web3.LAMPORTS_PER_SOL)*toSend //Investing 1 SOL. Remember 1 Lamport = 10^-9 SOL.
}),
);
// Setting the variables for the transaction
transaction.feePayer = await provider.publicKey;
let blockhashObj = await connection.getRecentBlockhash();
transaction.recentBlockhash = await blockhashObj.blockhash;
// Request creator to sign the transaction (allow the transaction)
let signed = await provider.signTransaction(transaction);
// The signature is generated
let signature = await connection.sendRawTransaction(signed.serialize());
// Confirm whether the transaction went through or not
console.log(await connection.confirmTransaction(signature));
//Signature chhap diya idhar
console.log("Signature: ", signature);
}
I'd like to specify that people will use phantom and I cant have access to their private keys (cause it was needed in all the answers I found on internet)
You're very close! You just need to replace the web3.SystemProgram.transfer instruction with an instruction to transfer SPL tokens, referencing the proper accounts. There's an example at the Solana Cookbook covering this exactly situation: https://solanacookbook.com/references/token.html#transfer-token
You can do this with help of anchor and spl-token, which is used for dealing with custom tokens on solana.
This is a custom transfer function. You'll need the mint address of the token, wallet from which the tokens will be taken( which you get in front end when user connects wallet. Can make use of solana-web3), to address and amount.
import * as splToken from "#solana/spl-token";
import { web3, Wallet } from "#project-serum/anchor";
async function transfer(tokenMintAddress: string, wallet: Wallet, to: string, connection: web3.Connection, amount: number) {
const mintPublicKey = new web3.PublicKey(tokenMintAddress);
const {TOKEN_PROGRAM_ID} = splToken
const fromTokenAccount = await splToken.getOrCreateAssociatedTokenAccount(
connection,
wallet.payer,
mintPublicKey,
wallet.publicKey
);
const destPublicKey = new web3.PublicKey(to);
// Get the derived address of the destination wallet which will hold the custom token
const associatedDestinationTokenAddr = await splToken.getOrCreateAssociatedTokenAccount(
connection,
wallet.payer,
mintPublicKey,
destPublicKey
);
const receiverAccount = await connection.getAccountInfo(associatedDestinationTokenAddr.address);
const instructions: web3.TransactionInstruction[] = [];
instructions.push(
splToken.createTransferInstruction(
fromTokenAccount.address,
associatedDestinationTokenAddr.address,
wallet.publicKey,
amount,
[],
TOKEN_PROGRAM_ID
)
);
const transaction = new web3.Transaction().add(...instructions);
transaction.feePayer = wallet.publicKey;
transaction.recentBlockhash = (await connection.getRecentBlockhash()).blockhash;
const transactionSignature = await connection.sendRawTransaction(
transaction.serialize(),
{ skipPreflight: true }
);
await connection.confirmTransaction(transactionSignature);
}

How to transfer NFT spl-token using phantom wallet and solana web3js

I can transfer the Solana from one account to another account using phantom wallet using this code
const transferTransaction = new Transaction()
.add(SystemProgram.transfer({
fromPubkey: alice.publicKey,
toPubkey: feePayer.publicKey,
lamports: lamportsToSend
}))
const network = "https://api.devnet.solana.com";
const connection = new Connection(network);
transferTransaction.recentBlockhash = (await connection.getRecentBlockhash()).blockhash;
transferTransaction.feePayer = alice.publicKey;
const { signature } = await window.solana.signAndSendTransaction(transferTransaction);
await connection.confirmTransaction(signature);
console.log(signature);
but I am wondering how can I transfer the NFT if I have the nft minted address?
To transfer an NFT, you first need to find out the address of the NFT's mint and the owner's address. Then instead of calling SystemProgram.transfer, you'll use Token.createTransferCheckedInstruction.
There's a great example at the Solana Cookbook for transferring SPL tokens: https://solanacookbook.com/references/token.html#transfer-token

hdwallet provider create wallet for user

I have created a smart contract that facilitates a token sale. I want a user to be able to press a button that automatically generates an ERC20 wallet that tokens can be sent to.
I am using the truffle HDWallet Provider and Infura. I don't want the user to have to use MetaMask or anything else. I will sign the transactions on the backend using the private key of the user's generated wallet.
How would I go about implementing this so a new wallet is created for every new user that wants to perform a transaction?
This is the code that allowed me to create a wallet on a button press. I now need to find a way to store the credentials of these wallets so I can sign transactions and transfer tokens to them.
var bip39 = require('bip39');
const EthereumUtil = require('ethereumjs-util');
const hdkey = require('hdkey');
const mnemonic = bip39.generateMnemonic(); //generates string
const seed = bip39.mnemonicToSeed(mnemonic); //creates seed buffer
const root = hdkey.fromMasterSeed(seed);
const masterPrivateKey = root.privateKey.toString('hex');
const addrNode = root.derive("m/44'/60'/0'/0/0");
const pubKey = EthereumUtil.privateToPublic(addrNode._privateKey);
const addr = EthereumUtil.publicToAddress(pubKey).toString('hex');
const address = EthereumUtil.toChecksumAddress(addr);
Its pretty simple, just create a HDWalletProvider and the provider.addresses has all the accounts you can use:
Here is the code:
const createWallet = function(_providerUrl) {
const mnemonicPhrase = bip39.generateMnemonic()
let provider = new HDWalletProvider({
mnemonic: {
phrase: mnemonicPhrase
},
providerOrUrl: "http://localhost:8545"
});
return provider.addresses
}
Addresses has the accounts
The solution has already been commented above, but I have a better suggestion for you. I think it's better to generate only one mnemonic in your backend and you create infinite accounts from that mnemonic. From each account you extract the private key and store it, store the address of the account and the index of the account.
Management code:
const HDWalletProvider = require("#truffle/hdwallet-provider");
const Web3 = require("web3");
const mnemonicPhrase = "mnemonicPhrase here";
const urlRPC = "https://bsc-dataseed.binance.org";
//Creating another instance to query balances
const Web3Instance = require('web3');
const web3instance = new Web3Instance(new Web3Instance.providers.HttpProvider(urlRPC));
//HD Wallet Provider loads by default only 10 accounts
//We change the limit to as many accounts as we want
//You cannot run the loop below for i greater than 10, because for
const limit = 50;
async function getAccount() {
provider = new HDWalletProvider({
mnemonic: mnemonicPhrase,
numberOfAddresses: limit,
providerOrUrl: urlRPC,
addressIndex: 0,
});
//HDWalletProvider is compatible with Web3
//Use it at Web3 constructor, just like any other Web3 Provider
const web3 = new Web3(provider);
//I realized that iterating a loop to limit always gives an error when it reaches limit
//This happens in this HD Wallet Provider package
//limit - 1 seems to work best
for (let i = 0; i < limit - 1; i ++) {
var get = await web3.eth.getAccounts()
var address = get[i];
var balanceETH = await web3instance.eth.getBalance(address)
//Checking the adress
console.log(address);
console.log(balanceETH);
}
}
getAccount();
Here is more information:
https://ethereum.stackexchange.com/questions/52370/web3-create-account-from-mnemonic-passphrase/144340#144340

Categories