I have been spending hours trying to understand why my ATA address owner suddenly changed to the token program instead of my account address. I am using PDA to transfer the token from the PDA ATA account but unable to do so since the PDA is no longer the ATA owner.
I tried testing Anchor to dissect the problem and to find solutions, here are my tests console logs:
Mint test result:
Mint: A2ojTC6aQZYP6bwUq1FmWN9kwaQTB7NKQmMs89j4FUkx
Sender ATA: 2KcR41e2NxnYY5DWDzvgzHiKpSoaZJ55kvBiqU111DaY
Sender ATA owner: 7QzoE1okkpgsn7Rx5pxyGDkXMSc3nsqhWitDHc6c8rKb
program ATA: H9SEYZsU5ao1WoUNoVTQjVMBbJLNjJmKA5N1cGfjxLqE
Supply: 100
PDA: 10
User: 90
✔ Mint token! (7001ms)
Mint test script:
it("Mint token!", async () => {
mintPubkey = await createMint(
program.provider.connection, // conneciton
user, // fee payer
user.publicKey, // mint authority
user.publicKey, // freeze authority (you can use `null` to disable it. when you disable it, you can't turn it on again)
9 // decimals
);
console.log("Mint:", mintPubkey.toBase58())
let tokenAccountPubkeyUser = await getOrCreateAssociatedTokenAccount(program.provider.connection, user, mintPubkey, user.publicKey)
console.log("Sender ATA:", tokenAccountPubkeyUser.address.toBase58())
let tokenAuth = await program.provider.connection.getAccountInfo(tokenAccountPubkeyUser.address);
console.log("Sender ATA owner:", tokenAccountPubkeyUser.owner.toBase58())
let tokenAccountPubkeyPda = await getOrCreateAssociatedTokenAccount(program.provider.connection, user, mintPubkey, program.programId)
console.log("program ATA:", tokenAccountPubkeyPda.address.toBase58())
let txhash = await mintToChecked(
program.provider.connection, // connection
user, // fee payer
mintPubkey, // mint
tokenAccountPubkeyUser.address, // receiver (sholud be a token account)
user, // mint authority
100e9, // amount. if your decimals is 9, you mint 10^9 for 1 token.
9 // decimals
);
let tokenSupply = await program.provider.connection.getTokenSupply(mintPubkey);
console.log("Supply:", tokenSupply.value.uiAmount)
txhash = await transferChecked(
program.provider.connection, // connection
user, // payer
tokenAccountPubkeyUser.address, // from (should be a token account)
mintPubkey, // mint
tokenAccountPubkeyPda.address, // to (should be a token account)
user, // from's owner
10e9, // amount, if your deciamls is 9, send 10^9 for 1 token
9 // decimals
);
let tokenAmount = await program.provider.connection.getTokenAccountBalance(tokenAccountPubkeyPda.address);
console.log("PDA:", tokenAmount.value.uiAmount)
let tokenAmountUser = await program.provider.connection.getTokenAccountBalance(tokenAccountPubkeyUser.address);
console.log("User:", tokenAmountUser.value.uiAmount)
})
Remove vault test result:
Mint: A2ojTC6aQZYP6bwUq1FmWN9kwaQTB7NKQmMs89j4FUkx
userProfilePDA CanbMWdj5UT8KWCAUwsmMyZFeyG8kWQER2tZQdxTohEK
Last vault: 3
vaultAccountPDA: 8tvqq4zWMGZuoe4tsjuC85WRQY8n5qxZeoyY2Ro7UwGi
vaultInfoPDA: A5E257kztkqdwxeqrjgFzjG2uPmECNX7LD96Vp6Tve7z
tokenProgram: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
receiver ATA: 2KcR41e2NxnYY5DWDzvgzHiKpSoaZJ55kvBiqU111DaY
receiver (user) ATA owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
sender (VaultInfo) ATA: BQhNK47ygEYARanGqJnSjKBco3Crot9ihDMJzT8u7yLU
VaultInfo ATA supply: 10
VaultATA owner: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Program owner: BPFLoaderUpgradeab1e11111111111111111111111
vaultInfoPDA owner: GsCu69BThDsobWHorHkNf8h8zobN6VsexiYkwkH2VtfV
Remove vault test script:
it("User Vault removed!", async () => {
// Add your test here.
// const tx = await program.methods.initializeUser().rpc();
console.log("user:", user.publicKey)
console.log("program:", program.programId)
console.log("token program:", TOKEN_PROGRAM_ID)
console.log("Mint:", mintPubkey.toBase58())
const [userProfilePDA] = await anchor.web3.PublicKey.findProgramAddress([
utf8.encode("USER_STATE"),
user.publicKey.toBuffer(),
],
program.programId
);
console.log("userProfilePDA", userProfilePDA.toBase58());
const userProfile = await program.account.userProfile.fetch(userProfilePDA);
// console.log("UserProfile:", userProfile)
console.log("Last vault:", userProfile.lastVault)
const [vaultAccountPDA] = await anchor.web3.PublicKey.findProgramAddress([
utf8.encode("VAULT_STATE"),
user.publicKey.toBuffer(),
new anchor.BN(0).toBuffer()
],
program.programId
);
console.log("vaultAccountPDA:", vaultAccountPDA.toBase58());
const [vaultInfoPDA] = await anchor.web3.PublicKey.findProgramAddress([
utf8.encode("INFO_STATE"),
// user.publicKey.toBuffer(),
],
program.programId
);
console.log("vaultInfoPDA:", vaultInfoPDA.toBase58());
let tokenAccountPubkeyUser = await getOrCreateAssociatedTokenAccount(program.provider.connection, user, mintPubkey, user.publicKey)
console.log("tokenProgram:", TOKEN_PROGRAM_ID.toBase58())
console.log("receiver ATA:", tokenAccountPubkeyUser.address.toBase58())
let tokenAuth = await program.provider.connection.getAccountInfo(tokenAccountPubkeyUser.address);
console.log("receiver (user) ATA owner:", tokenAuth.owner.toBase58())
let tokenAccountPubkeyVault = await getOrCreateAssociatedTokenAccount(program.provider.connection, user, mintPubkey, vaultInfoPDA, true)
console.log("sender (VaultInfo) ATA:", tokenAccountPubkeyVault.address.toBase58())
let txhash = await transferChecked(
program.provider.connection, // connection
user, // payer
tokenAccountPubkeyUser.address, // from (should be a token account)
mintPubkey, // mint
tokenAccountPubkeyVault.address, // to (should be a token account)
user, // from's owner
10e9, // amount, if your deciamls is 9, send 10^9 for 1 token
9 // decimals
);
let tokenAmount = await program.provider.connection.getTokenAccountBalance(tokenAccountPubkeyVault.address);
console.log("VaultInfo ATA supply:", tokenAmount.value.uiAmount)
tokenAuth = await program.provider.connection.getAccountInfo(tokenAccountPubkeyVault.address);
console.log("VaultATA owner:", tokenAuth.owner.toBase58())
tokenAuth = await program.provider.connection.getAccountInfo(program.programId);
console.log("Program owner:", tokenAuth.owner.toBase58())
tokenAuth = await program.provider.connection.getAccountInfo(vaultInfoPDA);
console.log("vaultInfoPDA owner:", tokenAuth.owner.toBase58())
const tx = await program.rpc.removeVault(0, {
accounts: {
authority: user.publicKey,
userProfile: userProfilePDA,
vaultAccount: vaultAccountPDA,
vaultInfo: vaultInfoPDA,
systemProgram: anchor.web3.SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
from: tokenAccountPubkeyVault.address,
to: tokenAccountPubkeyUser.address,
owner: vaultInfoPDA,
// sender: vaultInfoPDA
},
signers: []
})
console.log("Your transaction signature", tx);
});
I am expecting to use the vaultInfoPDA (which should be the owner of the ATA) to send the token out to user removing their vault.
Thanks!
The term owner is overloaded in the context of SPL tokens, which often causes confusion.
When you're logging the owner of the account after calling getAccountInfo, that gives you the program that owns the account, which must be the SPL Token program. The SPL Token program has the right to change the data in the account.
Within that account is also data. In that data, bytes 32-64 define the pubkey that can authorize movements from the account, the SPL token "owner". So there's two owners in one account, one defined by the Solana runtime, and another defined by the SPL Token program.
You can read more about the ownership model at https://docs.solana.com/developing/programming-model/accounts#ownership-and-assignment-to-programs
I am trying create an NFT mint, I know how to do it from CLI, but I need to do it from JS, I am looking at the code of #solana/spl-token package, but cannot find API analogous to
spl-token authorize <TOKEN_ID> mint --disable
Does anyone know how to do it?
You certainly can! Here's some code that doesn't compile to get you started:
import { Connection, PublicKey, Keypair } from '#solana/web3.js';
import { AuthorityType, setAuthority } from '#solana/spl-token';
const mint = new PublicKey("mint in base 58");
const payer = new Keypair(...);
const mintAuthority = new Keypair(...);
const connection = new Connection
await setAuthority(
connection,
payer,
mint,
mintAuthority,
AuthorityType.MintTokens,
null, // this sets the mint authority to null
);
Adapted from https://github.com/solana-labs/solana-program-library/blob/7c7507f4ec1f320bbc33af459d4d8b5573a6a906/token/js/test/e2e/setAuthority.test.ts#L88
What I would like to achieve is to make the user use the solana program for "free" and make the company pay for the transaction, what I have in mind is:
Extrapolate the transaction in the frontend
Send it to my backend server through an api
Use the wallet that I have on my BE to sing and set this wallet as payer
Send back the transaction
Sign the transaction with the user that is interacting with the FE
Send the transaction from the FE to the solana program.
Let's consider the hello world example https://github.com/solana-labs/example-helloworld
export async function sayHello(): Promise<void> {
console.log('Saying hello to', greetedPubkey.toBase58());
const instruction = new TransactionInstruction({
keys: [{pubkey: greetedPubkey, isSigner: false, isWritable: true}],
programId,
data: createSetInstruction()
});
console.log(instruction)
await sendAndConfirmTransaction(
connection,
new Transaction().add(instruction),
[payer],
);
}
I guess that in some way I could extrapolate the transaction before the sendAndConfirmTransaction
How can I achieve that and the sign it with my Backend wallet?
Update
In order to manage this problem, I started developing this service: cowsigner.com
You have the entire flow correct, so what you would do is:
const transaction = new Transaction(... your instructions here ...);
const wireTransaction = transaction.serialize();
// send wireTransaction to backend to be signed
// on backend:
const latestBlockhash = await connection.getLatestBlockhash();
transaction.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
transaction.recentBlockhash = latestBlockhash.blockhash;
transaction.feePayer = backendKey.publicKey;
transaction.partialSign(backendKey);
const wireTransactionToSendBack = transaction.serialize();
// send back wireTransactionToSendBack to frontend to be signed by user
// back on frontend:
transaction.partialSign(userKey); // or use a wallet adapter, most likely
const finalWireTransaction = transaction.serialize();
const signature = await connection.sendRawTransaction(finalWireTransaction);
I've been trying the code below to get the spl-token account address for a specific token in a Solana wallet from the Solana wallet address, but I am having issues getting the result I am looking for. I run:
const web3 = require('#solana/web3.js');
(async () => {
const solana = new web3.Connection("https://api.mainnet-beta.solana.com");
//the public solana address
const accountPublicKey = new web3.PublicKey(
"2B1Uy1UTnsaN1rBNJLrvk8rzTf5V187wkhouWJSApvGT"
);
//mintAccount = the token mint address
const mintAccount = new web3.PublicKey(
"GLmaRDRmYd4u3YLfnj9eq1mrwxa1YfSweZYYZXZLTRdK"
);
console.log(
await solana.getTokenAccountsByOwner(accountPublicKey, {
mint: mintAccount,
})
);
})();
I'm looking for the token account address in the return, 6kRT2kAVsBThd5cz6gaQtomaBwLxSp672RoRPGizikH4. I get:
{ context: { slot: 116402202 }, value: [ { account: [Object],
pubkey: [PublicKey] } ] }
I can drill down through this a bit using .value[0].pubkey or .value[0].account but ultimately can't get to the information i'm looking for, which is a return of 6kRT2kAVsBThd5cz6gaQtomaBwLxSp672RoRPGizikH4
Does anyone know what is going wrong?
(Note I do not want to use the getOrCreateAssociatedAccountInfo() method, i'm trying to get the token account address without handling the wallets keypair)
ISSUE SOLVED:
I needed to grab the correct _BN data and convert, solution below.
const web3 = require('#solana/web3.js');
(async () => {
const solana = new web3.Connection("https://api.mainnet-beta.solana.com");
//the public solana address
const accountPublicKey = new web3.PublicKey(
"2B1Uy1UTnsaN1rBNJLrvk8rzTf5V187wkhouWJSApvGT"
);
//mintAccount = the token mint address
const mintAccount = new web3.PublicKey(
"GLmaRDRmYd4u3YLfnj9eq1mrwxa1YfSweZYYZXZLTRdK"
);
const account = await solana.getTokenAccountsByOwner(accountPublicKey, {
mint: mintAccount});
console.log(account.value[0].pubkey.toString());
})();
That will certainly work as a heavy-handed approach. As an alternative, you can try to just search for the associated token account owned by that wallet without creating it if it doesn't exist.
You would essentially follow the logic at https://github.com/solana-labs/solana-program-library/blob/0a61bc4ea30f818d4c86f4fe1863100ed261c64d/token/js/client/token.js#L539
except without the whole catch block.
"Access keys are stored as account_id,public_key in a trie state."
how do I revoke an function-call access key from the blockchain?
You can remove an access key by sending a DeleteKey transaction. For more details on transactions, please checkout this page.
Here is how you would delete an access key for example.testnet:
const { KeyPair, keyStore, connect } = require("near-api-js");
const CREDENTIALS_DIR = "~/.near-credentials";
const ACCOUNT_ID = "example.testnet";
const PUBLIC_KEY = "8hSHprDq2StXwMtNd43wDTXQYsjXcD4MJTXQYsjXcc";
const keyStore = new keyStores.UnencryptedFileSystemKeyStore(CREDENTIALS_DIR);
const config = {
keyStore,
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
};
deleteAccessKey(ACCOUNT_ID, PUBLIC_KEY);
async function deleteAccessKey(accountId, publicKey) {
const near = await connect(config);
const account = await near.account(accountId);
await account.deleteKey(publicKey);
}
You will need to make sure you have credentials for your account stored locally to complete this function. Do this by running the following near-cli command:
near login
For more info on rotating access keys, check out this doc:
https://docs.near.org/docs/api/naj-cookbook#access-key-rotation