diff --git a/src/components/SignTransactionFormContent.js b/src/components/SignTransactionFormContent.js index 34840c4..6a3de27 100644 --- a/src/components/SignTransactionFormContent.js +++ b/src/components/SignTransactionFormContent.js @@ -9,6 +9,7 @@ import { useConnection, useSolanaExplorerUrlSuffix } from '../utils/connection'; import { useWallet, useWalletPublicKeys } from '../utils/wallet'; import NewOrder from './instructions/NewOrder'; import UnknownInstruction from './instructions/UnknownInstruction'; +import StakeInstruction from '../components/instructions/StakeInstruction'; import SystemInstruction from '../components/instructions/SystemInstruction'; import DexInstruction from '../components/instructions/DexInstruction'; import TokenInstruction from '../components/instructions/TokenInstruction'; @@ -215,6 +216,19 @@ export default function SignTransactionFormContent({ onOpenAddress={onOpenAddress} /> ); + case 'stakeAuthorizeWithSeed': + case 'stakeAuthorize': + case 'stakeDeactivate': + case 'stakeDelegate': + case 'stakeInitialize': + case 'stakeSplit': + case 'stakeWithdraw': + return ( + + ); case 'newOrder': return ( diff --git a/src/components/instructions/StakeInstruction.js b/src/components/instructions/StakeInstruction.js new file mode 100644 index 0000000..b04aca2 --- /dev/null +++ b/src/components/instructions/StakeInstruction.js @@ -0,0 +1,65 @@ +import React from 'react'; +import Typography from '@material-ui/core/Typography'; +import LabelValue from './LabelValue'; + +const TYPE_LABELS = { + stakeAuthorizeWithSeed: 'Stake authorize with seed', + stakeAuthorize: 'Stake authorize', + stakeDeactivate: 'Deactivate stake', + stakeDelegate: 'Delegate stake', + stakeInitialize: 'Initialize stake', + stakeSplit: 'Split stake', + stakeWithdraw: 'Withdraw stake', +}; + +const DATA_LABELS = { + stakePubkey: { label: 'Stake', address: true }, + authorizedStaker: { label: 'Authorized staker', address: true }, + authorizedWithdrawer: { label: 'Authorized withdrawer', address: true }, + lockup: { label: 'Lockup', address: false }, + authorizedPubkey: { label: 'Authorized', address: true }, + votePubkey: { label: 'Vote', address: true }, + authorizedSeed: { label: 'Seed', address: false }, + noncePubkey: { label: 'Nonce', address: true }, + authorityBase: { label: 'Authority base', address: true }, + authoritySeed: { label: 'Authority seed', address: false }, + authorityOwner: { label: 'Authority owner', address: true }, + newAuthorizedPubkey: { label: 'New authorized', address: true }, + stakeAuthorizationType: { label: 'Stake authorization type', address: false, transform: () => JSON.stringify }, + custodianPubkey: { label: 'Custodian', address: true }, + splitStakePubkey: { label: 'Split to', address: true }, + lamports: { label: 'Lamports', address: false }, +}; + +export default function StakeInstruction({ instruction, onOpenAddress }) { + const { type, data } = instruction; + + return ( + <> + + {TYPE_LABELS[type]} + + {data && + Object.entries(data).map(([key, value]) => { + const dataLabel = DATA_LABELS[key]; + if (!dataLabel) { + return null; + } + const { label, address, transform } = dataLabel; + return ( + address && onOpenAddress(value?.toBase58())} + /> + ); + })} + + ); +} diff --git a/src/utils/transactions.js b/src/utils/transactions.js index 62f23c3..1130201 100644 --- a/src/utils/transactions.js +++ b/src/utils/transactions.js @@ -1,5 +1,5 @@ import bs58 from 'bs58'; -import { Message, SystemInstruction, SystemProgram } from '@solana/web3.js'; +import { Message, StakeInstruction, StakeProgram, SystemInstruction, SystemProgram } from '@solana/web3.js'; import { decodeInstruction, decodeTokenInstructionData, @@ -96,6 +96,9 @@ const toInstruction = async ( if (programId.equals(SystemProgram.programId)) { console.log('[' + index + '] Handled as system instruction'); return handleSystemInstruction(publicKey, instruction, accountKeys); + } else if (programId.equals(StakeProgram.programId)) { + console.log('[' + index + '] Handled as stake instruction'); + return handleStakeInstruction(publicKey, instruction, accountKeys); } else if (programId.equals(TOKEN_PROGRAM_ID)) { console.log('[' + index + '] Handled as token instruction'); let decodedInstruction = decodeTokenInstruction(decoded); @@ -155,7 +158,9 @@ const toInstruction = async ( programId, }; } - } catch {} + } catch (e) { + console.log(`Failed to decode instruction: ${e}`); + } // all decodings failed console.log('[' + index + '] Failed, data: ' + JSON.stringify(decoded)); @@ -380,6 +385,74 @@ const handleSystemInstruction = (publicKey, instruction, accountKeys) => { }; }; +const handleStakeInstruction = (publicKey, instruction, accountKeys) => { + const { programIdIndex, accounts, data } = instruction; + if (!programIdIndex || !accounts || !data) { + return; + } + + // construct stake instruction + const stakeInstruction = { + programId: accountKeys[programIdIndex], + keys: accounts.map((accountIndex) => ({ + pubkey: accountKeys[accountIndex], + })), + data: bs58.decode(data), + }; + + let decoded; + const type = StakeInstruction.decodeInstructionType(stakeInstruction); + switch (type) { + case 'AuthorizeWithSeed': + decoded = StakeInstruction.decodeAuthorizeWithSeed(stakeInstruction); + break; + case 'Authorize': + decoded = StakeInstruction.decodeAuthorize(stakeInstruction); + break; + case 'Deactivate': + decoded = StakeInstruction.decodeDeactivate(stakeInstruction); + break; + case 'Delegate': + decoded = StakeInstruction.decodeDelegate(stakeInstruction); + break; + case 'Initialize': + decoded = StakeInstruction.decodeInitialize(stakeInstruction); + // Lockup inactive if all zeroes + const lockup = decoded.lockup; + if (lockup && lockup.unixTimestamp === 0 && lockup.epoch === 0 && lockup.custodian.equals(PublicKey.default)) { + decoded.lockup = 'Inactive'; + } + else { + decoded.lockup = `unixTimestamp: ${lockup.unixTimestamp}, custodian: ${lockup.epoch}, custodian: ${lockup.custodian.toBase58()}`; + } + // flatten authorized to allow address render + decoded.authorizedStaker = decoded.authorized.staker + decoded.authorizedWithdrawer = decoded.authorized.withdrawer + delete decoded.authorized + break; + case 'Split': + decoded = StakeInstruction.decodeSplit(stakeInstruction); + break; + case 'Withdraw': + decoded = StakeInstruction.decodeWithdraw(stakeInstruction); + break; + default: + return; + } + + if ( + !decoded || + (decoded.fromPubkey && !publicKey.equals(decoded.fromPubkey)) + ) { + return; + } + + return { + type: 'stake' + type, + data: decoded, + }; +}; + const handleTokenInstruction = ( publicKey, accounts,