1- import { getAssociatedTokenAddress } from '@solana/spl-token' ;
1+ import {
2+ decodeInstruction ,
3+ getAssociatedTokenAddress ,
4+ isTransferCheckedInstruction ,
5+ isTransferInstruction ,
6+ } from '@solana/spl-token' ;
27import {
38 ConfirmedTransactionMeta ,
49 Connection ,
510 Finality ,
611 LAMPORTS_PER_SOL ,
712 Message ,
13+ SystemInstruction ,
14+ Transaction ,
815 TransactionResponse ,
916 TransactionSignature ,
1017} from '@solana/web3.js' ;
1118import BigNumber from 'bignumber.js' ;
12- import { Amount , Memo , Recipient , References , SPLToken } from './types' ;
19+ import { MEMO_PROGRAM_ID } from './constants' ;
20+ import { Amount , Memo , Recipient , Reference , References , SPLToken } from './types' ;
1321
1422/**
1523 * Thrown when a transaction doesn't contain a valid Solana Pay transfer.
@@ -58,33 +66,49 @@ export async function validateTransfer(
5866 if ( ! meta ) throw new ValidateTransferError ( 'missing meta' ) ;
5967 if ( meta . err ) throw meta . err ;
6068
61- const [ preAmount , postAmount ] = splToken
62- ? await validateSPLTokenTransfer ( message , meta , recipient , splToken )
63- : await validateSystemTransfer ( message , meta , recipient ) ;
69+ if ( reference && ! Array . isArray ( reference ) ) {
70+ reference = [ reference ] ;
71+ }
6472
73+ const [ preAmount , postAmount ] = splToken
74+ ? await validateSPLTokenTransfer ( message , meta , recipient , splToken , reference )
75+ : await validateSystemTransfer ( message , meta , recipient , reference ) ;
6576 if ( postAmount . minus ( preAmount ) . lt ( amount ) ) throw new ValidateTransferError ( 'amount not transferred' ) ;
6677
67- if ( reference ) {
68- if ( ! Array . isArray ( reference ) ) {
69- reference = [ reference ] ;
70- }
71-
72- for ( const pubkey of reference ) {
73- if ( ! message . accountKeys . some ( ( accountKey ) => accountKey . equals ( pubkey ) ) )
74- throw new ValidateTransferError ( 'reference not found' ) ;
75- }
78+ if ( memo ) {
79+ // Check that the second instruction is a memo instruction with the expected memo.
80+ const transaction = Transaction . populate ( message ) ;
81+ const instruction = transaction . instructions [ 1 ] ;
82+ if ( ! instruction ) throw new ValidateTransferError ( 'missing memo instruction' ) ;
83+ if ( ! instruction . programId . equals ( MEMO_PROGRAM_ID ) ) throw new ValidateTransferError ( 'invalid memo program' ) ;
84+ if ( ! instruction . data . equals ( Buffer . from ( memo , 'utf8' ) ) ) throw new ValidateTransferError ( 'invalid memo' ) ;
7685 }
7786
78- // FIXME: add memo check
79-
8087 return response ;
8188}
8289
8390async function validateSystemTransfer (
8491 message : Message ,
8592 meta : ConfirmedTransactionMeta ,
86- recipient : Recipient
93+ recipient : Recipient ,
94+ references ?: Reference [ ]
8795) : Promise < [ BigNumber , BigNumber ] > {
96+ if ( references ) {
97+ // Check that the first instruction is a system transfer instruction.
98+ const transaction = Transaction . populate ( message ) ;
99+ const instruction = transaction . instructions [ 0 ] ;
100+ SystemInstruction . decodeTransfer ( instruction ) ;
101+
102+ // Check that the expected reference keys exactly match the extra keys provided to the instruction.
103+ const [ _from , _to , ...extraKeys ] = instruction . keys ;
104+ const length = extraKeys . length ;
105+ if ( length !== references . length ) throw new ValidateTransferError ( 'invalid references' ) ;
106+
107+ for ( let i = 0 ; i < length ; i ++ ) {
108+ if ( ! extraKeys [ i ] . pubkey . equals ( references [ i ] ) ) throw new ValidateTransferError ( `invalid reference ${ i } ` ) ;
109+ }
110+ }
111+
88112 const accountIndex = message . accountKeys . findIndex ( ( pubkey ) => pubkey . equals ( recipient ) ) ;
89113 if ( accountIndex === - 1 ) throw new ValidateTransferError ( 'recipient not found' ) ;
90114
@@ -98,8 +122,26 @@ async function validateSPLTokenTransfer(
98122 message : Message ,
99123 meta : ConfirmedTransactionMeta ,
100124 recipient : Recipient ,
101- splToken : SPLToken
125+ splToken : SPLToken ,
126+ references ?: Reference [ ]
102127) : Promise < [ BigNumber , BigNumber ] > {
128+ if ( references ) {
129+ // Check that the first instruction is an SPL token transfer instruction.
130+ const transaction = Transaction . populate ( message ) ;
131+ const instruction = decodeInstruction ( transaction . instructions [ 0 ] ) ;
132+ if ( ! isTransferCheckedInstruction ( instruction ) && ! isTransferInstruction ( instruction ) )
133+ throw new ValidateTransferError ( 'invalid transfer' ) ;
134+
135+ // Check that the expected reference keys exactly match the extra keys provided to the instruction.
136+ const extraKeys = instruction . keys . multiSigners ;
137+ const length = extraKeys . length ;
138+ if ( length !== references . length ) throw new ValidateTransferError ( 'invalid references' ) ;
139+
140+ for ( let i = 0 ; i < length ; i ++ ) {
141+ if ( ! extraKeys [ i ] . pubkey . equals ( references [ i ] ) ) throw new ValidateTransferError ( `invalid reference ${ i } ` ) ;
142+ }
143+ }
144+
103145 const recipientATA = await getAssociatedTokenAddress ( splToken , recipient ) ;
104146 const accountIndex = message . accountKeys . findIndex ( ( pubkey ) => pubkey . equals ( recipientATA ) ) ;
105147 if ( accountIndex === - 1 ) throw new ValidateTransferError ( 'recipient not found' ) ;
0 commit comments