@@ -36,13 +36,15 @@ import {
3636 SignedTransaction ,
3737 SignTransactionOptions as BaseSignTransactionOptions ,
3838 TokenEnablementConfig ,
39- TransactionExplanation ,
39+ TransactionParams ,
40+ TransactionType ,
4041 VerifyAddressOptions ,
4142 VerifyTransactionOptions ,
4243} from '@bitgo/sdk-core' ;
4344import { BaseCoin as StaticsBaseCoin , CoinFamily , coins , Nep141Token , Networks } from '@bitgo/statics' ;
4445
4546import { KeyPair as NearKeyPair , Transaction , TransactionBuilder , TransactionBuilderFactory } from './lib' ;
47+ import { TransactionExplanation , TxData } from './lib/iface' ;
4648import nearUtils from './lib/utils' ;
4749import { MAX_GAS_LIMIT_FOR_FT_TRANSFER } from './lib/constants' ;
4850
@@ -1000,6 +1002,10 @@ export class Near extends BaseCoin {
10001002 const explainedTx = transaction . explainTransaction ( ) ;
10011003
10021004 // users do not input recipients for consolidation requests as they are generated by the server
1005+ if ( txParams . type === 'enabletoken' && params . verification ?. verifyTokenEnablement ) {
1006+ this . validateTokenEnablementTransaction ( transaction , explainedTx , txParams ) ;
1007+ }
1008+
10031009 if ( txParams . recipients !== undefined ) {
10041010 if ( txParams . type === 'enabletoken' ) {
10051011 const tokenName = explainedTx . outputs [ 0 ] . tokenName ;
@@ -1031,6 +1037,18 @@ export class Near extends BaseCoin {
10311037 } ) ;
10321038
10331039 if ( ! _ . isEqual ( filteredOutputs , filteredRecipients ) ) {
1040+ // For enabletoken, provide more specific error messages for address mismatches
1041+ if ( txParams . type === 'enabletoken' && params . verification ?. verifyTokenEnablement ) {
1042+ const mismatchedAddresses = txParams . recipients
1043+ ?. filter (
1044+ ( recipient , index ) => ! filteredOutputs [ index ] || recipient . address !== filteredOutputs [ index ] . address
1045+ )
1046+ . map ( ( recipient ) => recipient . address ) ;
1047+
1048+ if ( mismatchedAddresses && mismatchedAddresses . length > 0 ) {
1049+ throw new Error ( `Address mismatch: ${ mismatchedAddresses . join ( ', ' ) } ` ) ;
1050+ }
1051+ }
10341052 throw new Error ( 'Tx outputs does not match with expected txParams recipients' ) ;
10351053 }
10361054 for ( const recipients of txParams . recipients ) {
@@ -1055,4 +1073,199 @@ export class Near extends BaseCoin {
10551073 }
10561074 auditEddsaPrivateKey ( prv , publicKey ?? '' ) ;
10571075 }
1076+
1077+ private validateTokenEnablementTransaction (
1078+ transaction : Transaction ,
1079+ explainedTx : TransactionExplanation ,
1080+ txParams : TransactionParams
1081+ ) : void {
1082+ const transactionData = transaction . toJson ( ) ;
1083+ this . validateTxType ( txParams , explainedTx ) ;
1084+ this . validateSigner ( transactionData ) ;
1085+ this . validateRawReceiver ( transactionData , txParams ) ;
1086+ this . validatePublicKey ( transactionData ) ;
1087+ this . validateRawActions ( transactionData , txParams ) ;
1088+ this . validateBeneficiary ( explainedTx , txParams ) ;
1089+ this . validateTokenOutput ( explainedTx , txParams ) ;
1090+ }
1091+
1092+ // Validates that the signer ID exists in the transaction
1093+ private validateSigner ( transactionData : TxData ) : void {
1094+ if ( ! transactionData . signerId ) {
1095+ throw new Error ( 'Error on token enablements: missing signer ID in transaction' ) ;
1096+ }
1097+ }
1098+
1099+ private validateBeneficiary ( explainedTx : TransactionExplanation , txParams : TransactionParams ) : void {
1100+ if ( ! explainedTx . outputs || explainedTx . outputs . length === 0 ) {
1101+ throw new Error ( 'Error on token enablements: transaction has no outputs to validate beneficiary' ) ;
1102+ }
1103+
1104+ const output = explainedTx . outputs [ 0 ] ;
1105+ const recipient = txParams . recipients ?. [ 0 ] ;
1106+
1107+ if ( ! recipient ?. address ) {
1108+ throw new Error ( 'Error on token enablements: missing beneficiary address in transaction parameters' ) ;
1109+ }
1110+
1111+ if ( output . address !== recipient . address ) {
1112+ throw new Error ( 'Error on token enablements: transaction beneficiary mismatch with user expectation' ) ;
1113+ }
1114+ }
1115+
1116+ // Validates that the raw transaction receiverId matches the expected token contract
1117+ private validateRawReceiver ( transactionData : TxData , txParams : TransactionParams ) : void {
1118+ if ( ! transactionData . receiverId ) {
1119+ throw new Error ( 'Error on token enablements: missing receiver ID in transaction' ) ;
1120+ }
1121+
1122+ const recipient = txParams . recipients ?. [ 0 ] ;
1123+ if ( ! recipient ?. tokenName ) {
1124+ throw new Error ( 'Error on token enablements: missing token name in transaction parameters' ) ;
1125+ }
1126+
1127+ const tokenInstance = nearUtils . getTokenInstanceFromTokenName ( recipient . tokenName ) ;
1128+ if ( ! tokenInstance ) {
1129+ throw new Error ( `Error on token enablements: unknown token '${ recipient . tokenName } '` ) ;
1130+ }
1131+
1132+ if ( transactionData . receiverId !== tokenInstance . contractAddress ) {
1133+ throw new Error (
1134+ `Error on token enablements: receiver contract mismatch - expected '${ tokenInstance . contractAddress } ', got '${ transactionData . receiverId } '`
1135+ ) ;
1136+ }
1137+ }
1138+
1139+ // Validates token output information from explained transaction
1140+ private validateTokenOutput ( explainedTx : TransactionExplanation , txParams : TransactionParams ) : void {
1141+ if ( ! explainedTx . outputs || explainedTx . outputs . length !== 1 ) {
1142+ throw new Error ( 'Error on token enablements: transaction must have exactly 1 output' ) ;
1143+ }
1144+
1145+ const output = explainedTx . outputs [ 0 ] ;
1146+ const recipient = txParams . recipients ?. [ 0 ] ;
1147+
1148+ if ( ! output . tokenName ) {
1149+ throw new Error ( 'Error on token enablements: missing token name in transaction output' ) ;
1150+ }
1151+
1152+ const tokenInstance = nearUtils . getTokenInstanceFromTokenName ( output . tokenName ) ;
1153+ if ( ! tokenInstance ) {
1154+ throw new Error ( `Error on token enablements: unknown token '${ output . tokenName } '` ) ;
1155+ }
1156+
1157+ if ( recipient ?. tokenName && recipient . tokenName !== output . tokenName ) {
1158+ throw new Error (
1159+ `Error on token enablements: token mismatch - user expects '${ recipient . tokenName } ', transaction has '${ output . tokenName } '`
1160+ ) ;
1161+ }
1162+ }
1163+
1164+ private validatePublicKey ( transactionData : TxData ) : void {
1165+ if ( ! transactionData . publicKey ) {
1166+ throw new Error ( 'Error on token enablements: missing public key in transaction' ) ;
1167+ }
1168+
1169+ // Validate ed25519 format: "ed25519:base58_encoded_key"
1170+ if ( ! transactionData . publicKey . startsWith ( 'ed25519:' ) ) {
1171+ throw new Error ( 'Error on token enablements: unsupported key type, expected ed25519' ) ;
1172+ }
1173+
1174+ // Validate base58 part after "ed25519:"
1175+ const base58Part = transactionData . publicKey . substring ( 8 ) ;
1176+ if ( ! base58Part || base58Part . length !== 44 ) {
1177+ // ed25519 keys are 32 bytes = 44 base58 chars
1178+ throw new Error ( 'Error on token enablements: invalid ed25519 public key format' ) ;
1179+ }
1180+
1181+ // Validate it's actually valid base58
1182+ let decoded ;
1183+ try {
1184+ decoded = nearAPI . utils . serialize . base_decode ( base58Part ) ;
1185+ } catch {
1186+ throw new Error ( 'Error on token enablements: invalid base58 encoding in public key' ) ;
1187+ }
1188+
1189+ if ( ! decoded || decoded . length !== 32 ) {
1190+ throw new Error ( 'Error on token enablements: invalid ed25519 public key length' ) ;
1191+ }
1192+ }
1193+
1194+ // Validates the raw transaction actions according to NEAR protocol spec
1195+ private validateRawActions ( transactionData : TxData , txParams : TransactionParams ) : void {
1196+ // Must have exactly 1 action (NEAR spec requirement)
1197+ if ( ! transactionData . actions || transactionData . actions . length !== 1 ) {
1198+ throw new Error ( 'Error on token enablements: must have exactly 1 action' ) ;
1199+ }
1200+
1201+ const action = transactionData . actions [ 0 ] ;
1202+
1203+ // Must be a functionCall action (not transfer)
1204+ if ( ! action . functionCall ) {
1205+ throw new Error ( 'Error on token enablements: action must be a function call' ) ;
1206+ }
1207+
1208+ // Must be storage_deposit method (NEAR spec requirement)
1209+ if ( action . functionCall . methodName !== 'storage_deposit' ) {
1210+ throw new Error (
1211+ `Error on token enablements: invalid method '${ action . functionCall . methodName } ', expected 'storage_deposit'`
1212+ ) ;
1213+ }
1214+
1215+ // Validate args structure (should be JSON object)
1216+ if ( ! action . functionCall . args || typeof action . functionCall . args !== 'object' ) {
1217+ throw new Error ( 'Error on token enablements: invalid or missing function call arguments' ) ;
1218+ }
1219+
1220+ // Validate deposit exists and is valid
1221+ if ( ! action . functionCall . deposit ) {
1222+ throw new Error ( 'Error on token enablements: missing deposit in function call' ) ;
1223+ }
1224+
1225+ const depositAmount = new BigNumber ( action . functionCall . deposit ) ;
1226+ if ( depositAmount . isNaN ( ) || depositAmount . isLessThan ( 0 ) ) {
1227+ throw new Error ( 'Error on token enablements: invalid deposit amount in function call' ) ;
1228+ }
1229+
1230+ // Validate gas exists and is valid
1231+ if ( ! action . functionCall . gas ) {
1232+ throw new Error ( 'Error on token enablements: missing gas in function call' ) ;
1233+ }
1234+
1235+ const gasAmount = new BigNumber ( action . functionCall . gas ) ;
1236+ if ( gasAmount . isNaN ( ) || gasAmount . isLessThan ( 0 ) ) {
1237+ throw new Error ( 'Error on token enablements: invalid gas amount in function call' ) ;
1238+ }
1239+
1240+ // Validate deposit amount against expected storage deposit (merged from validateActions)
1241+ const recipient = txParams . recipients ?. [ 0 ] ;
1242+ if ( recipient ?. tokenName ) {
1243+ const tokenInstance = nearUtils . getTokenInstanceFromTokenName ( recipient . tokenName ) ;
1244+ if ( tokenInstance ?. storageDepositAmount && action . functionCall . deposit !== tokenInstance . storageDepositAmount ) {
1245+ throw new Error (
1246+ `Error on token enablements: deposit amount ${ action . functionCall . deposit } does not match expected storage deposit ${ tokenInstance . storageDepositAmount } `
1247+ ) ;
1248+ }
1249+ }
1250+
1251+ // Validate user-specified amount matches deposit (merged from validateActions)
1252+ if (
1253+ recipient ?. amount !== undefined &&
1254+ recipient . amount !== '0' &&
1255+ recipient . amount !== action . functionCall . deposit
1256+ ) {
1257+ throw new Error (
1258+ `Error on token enablements: user specified amount '${ recipient . amount } ' does not match storage deposit '${ action . functionCall . deposit } '`
1259+ ) ;
1260+ }
1261+ }
1262+
1263+ private validateTxType ( txParams : TransactionParams , explainedTx : TransactionExplanation ) : void {
1264+ const expectedType = TransactionType . StorageDeposit ;
1265+ const actualType = explainedTx . type ;
1266+
1267+ if ( actualType !== expectedType ) {
1268+ throw new Error ( `Invalid transaction type on token enablement: expected "${ expectedType } ", got "${ actualType } ".` ) ;
1269+ }
1270+ }
10581271}
0 commit comments