Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ups 889 robo wallet can send solana token lockup transactions #17

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
"required": true,
"value": "4"
},
"FIGMENT_API_KEY": {
"description": "API key for Figment. Required to use with Solana blockchain.",
"required": false
},
"OPT_IN_APP": {
"description": "Algorand app to auto opt-in. Required to use with algorand blockchain.",
"required": false
Expand Down
21 changes: 20 additions & 1 deletion lib/SolanaTxHelper.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,25 @@ import {
u64 as U64
} from '@solana/spl-token'

import TokenLockupTxHelper from './TokenLockupHelper.mjs'

export default class SolanaTxHelper {
constructor(network) {
constructor(network, provider) {
const networks = {
'solana': 'mainnet-beta',
'solana_devnet': 'devnet',
'solana_testnet': 'testnet'
}

this.connection = new Connection(clusterApiUrl(networks[network]))
this.provider = provider
}

async sendRawTransaction(signed) {
return await this.connection.sendRawTransaction(
signed.serialize(),
{ skipPreflight: true }
)
}

async confirmTransaction(signature) {
Expand All @@ -40,6 +50,10 @@ export default class SolanaTxHelper {
case 'Blockchain::Solana::Tx::Spl::BatchTransfer':
transaction = await this.createSPLBatchTransfer(obj)
break
case 'Blockchain::Solana::Tx::SplLockup::FundReleaseSchedule':
case 'Blockchain::Solana::Tx::SplLockup::BatchFundReleaseSchedule':
transaction = await this.createSplLockup(obj)
break
default:
throw new Error(`Unsupported transaction type: ${obj.type}`)
}
Expand All @@ -52,6 +66,11 @@ export default class SolanaTxHelper {
return transaction
}

async createSplLockup(obj) {
const tokenLockupTxHelper = new TokenLockupTxHelper(this.connection, this.provider, obj.programId)
return await tokenLockupTxHelper.create(obj)
}

createSystemProgramTransferTransaction(obj) {
if (typeof obj.from !== 'string' || typeof obj.to !== 'string'
|| !SolanaTxHelper.isSolanaAmount(obj.amount.toString())) {
Expand Down
262 changes: 262 additions & 0 deletions lib/TokenLockupHelper.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import { TOKEN_LOCKUP_ABI } from './splTockenlockupAbi.js'

import {
Transaction,
PublicKey,
} from '@solana/web3.js'

import {
TOKEN_PROGRAM_ID,
Token
} from '@solana/spl-token'

import anchor from '@project-serum/anchor'

import BigNumber from 'bignumber.js'
import SolanaTxHelper from './SolanaTxHelper.mjs'

const SOLANA_MAX_TRANSACTION_SIZE = 1232
const SOLANA_SIGNATURE_SIZE = 64
const SOLANA_MAX_TRANSACTION_UNSIGNED_SIZE = SOLANA_MAX_TRANSACTION_SIZE - SOLANA_SIGNATURE_SIZE

function getNonceBytes(value) {
if(isNaN(value)) {
throw new Error("Nonce must be numeric")
}
const valueStr = value.toString(16)
const MAX_LENGTH = 16

for (var bytes = [], c = 0; c < valueStr.length; c += 2) {
const num = parseInt(valueStr.substr(c, 2), 16)
bytes.push(isNaN(num) ? 0 : num)
}

while (bytes.length < MAX_LENGTH) {
bytes = [0].concat(bytes)
}

return bytes
}

export default class TokenLockupTxHelper {
constructor(connection, wallet, programId) {
this.connection = connection
const _idl = TOKEN_LOCKUP_ABI
console.log(programId)
this.programId = new PublicKey(programId)
const provider = new anchor.Provider(
connection, wallet, 'confirmed',
)
this.program = new anchor.Program(_idl, this.programId, provider)
this.provider = wallet
}

async create(obj) {
switch (obj.type) {
case 'Blockchain::Solana::Tx::SplLockup::FundReleaseSchedule':
return await this.fundReleaseSchedule(obj)
case 'Blockchain::Solana::Tx::SplLockup::BatchFundReleaseSchedule':
return await this.batchFundReleaseSchedule(obj)
default:
throw new Error(`Unsupported transaction type: ${obj.type}`)
}
}

/**
* Transfer amount to vesting schedule account for future payouts
* @param {object} obj - Sample of the object with data is shown above
* @returns {Transaction} - creates Transaction with set of instruction for FundReleaseSchedule
*/
async fundReleaseSchedule(obj) {
if (isNaN(obj.scheduleId) || parseInt(obj.scheduleId) < 0
|| !SolanaTxHelper.isSolanaAmount(obj.amount.toString())) {
throw new Error(`Unsupported fund release schedule parameters: ${obj.scheduleId}, ${obj.amount}`)
}
if (obj.cancelableBy.length > 10) {
throw new Error(`Cancalable by accounts more than 10: ${obj.cancelableBy.length}`)
}

const tokenAccPubKey = new PublicKey(obj.tokenLockAddress)
const tokenlockAcc = await this.connection.getAccountInfo(tokenAccPubKey)
if (!tokenlockAcc) {
throw new Error('Cannot find locked data account')
}

const fromPubKey = new PublicKey(obj.from)
const authAcc = fromPubKey
const toPubKey = new PublicKey(obj.to)
const cancelableBy = obj.cancelableBy.map(element => new PublicKey(element))
const nonceBytes = getNonceBytes(new BigNumber(obj.nonce))

const mintToken = await this.createMint(obj.tokenMintAddress, tokenAccPubKey)
const fromTokenAccount = await mintToken.getOrCreateAssociatedAccountInfo(fromPubKey)
const [toTokenAccount, getAssociatedTokenAddressInstr] =
await this.getAssociatedTokenAddress(mintToken, toPubKey, fromPubKey)

let instructions = []
if (getAssociatedTokenAddressInstr !== null) {
instructions.push(getAssociatedTokenAddressInstr)
}

const insr = await this.fundReleaseScheduleInstruction(
obj.amount,
obj.commencementTimestamp,
obj.scheduleId,
cancelableBy,
tokenAccPubKey,
authAcc,
fromTokenAccount.address,
toTokenAccount,
nonceBytes
)

instructions.push(insr)

return new Transaction().add(...instructions)
}

async fundReleaseScheduleInstruction(
amount, commencementTimestamp,
scheduleId, cancelableBy,
tokenlockAcc, authAcc, from, to, nonce) {
const tokenlockData = await this.program.account.tokenLockData.fetch(tokenlockAcc)

return this.program.instruction.fundReleaseSchedule(
nonce,
new anchor.BN(amount),
new anchor.BN(commencementTimestamp),
scheduleId,
cancelableBy,
{
accounts: {
tokenlockAccount: tokenlockAcc,
escrowAccount: tokenlockData.escrowAccount,
from: from,
to: to,
authority: authAcc,
tokenProgram: TOKEN_PROGRAM_ID
}
})
}

/**
* Create set of transfers to vesting schedule account for future payouts
* @param {object} obj - Sample of the object with data is shown above
* @returns {Transaction} - creates Transaction with set of instruction for BatchFundReleaseSchedule
*/
async batchFundReleaseSchedule(obj) {
if (!Array.isArray(obj.amount) || !Array.isArray(obj.commencementTimestamp)
|| !Array.isArray(obj.scheduleId) || !Array.isArray(obj.nonce) || !Array.isArray(obj.to)) {
throw new Error('Incorrect parameters input')
}
if (obj.amount.length !== obj.commencementTimestamp.length ||
obj.amount.length !== obj.scheduleId.length ||
obj.amount.length !== obj.nonce.length ||
obj.amount.length !== obj.to.length) {
throw new Error('Incorrect parameter array length')
}
if (obj.cancelableBy.length > 10) {
throw new Error(`Cancalable by accounts more than 10: ${obj.cancelableBy.length}`)
}

const mintAddress = obj.tokenMintAddress
const fromPubKey = new PublicKey(obj.from)
const authAcc = fromPubKey // or provider.walletAddress ??
const cancelableBy = obj.cancelableBy.map(element => new PublicKey(element))
const tokenAccPubKey = new PublicKey(obj.tokenLockAddress)

const tokenlockAcc = await this.connection.getAccountInfo(tokenAccPubKey)
if (!tokenlockAcc) {
throw new Error('Cannot find locked data account')
}
const mintToken = await this.createMint(mintAddress, tokenAccPubKey)
const fromTokenAccount = await mintToken.getOrCreateAssociatedAccountInfo(fromPubKey)

let instructions = []
for (let i = 0; i < obj.amount.length; i++) {
if (isNaN(obj.scheduleId[i]) || parseInt(obj.scheduleId[i]) < 0
|| !SolanaTxHelper.isSolanaAmount(obj.amount[i].toString())) {
throw new Error(`Unsupported fund release schedule [${i}] parameters: ${obj.scheduleId[i]}, ${obj.amount[i]}`)
}

const nonceBytes = getNonceBytes(new BigNumber(obj.nonce[i]))
const [toTokenAccount, getAssociatedTokenAddressInstr] =
await this.getAssociatedTokenAddress(mintToken, new PublicKey(obj.to[i]), fromPubKey)

if (getAssociatedTokenAddressInstr !== null) {
instructions.push(getAssociatedTokenAddressInstr)
}

instructions.push(await this.fundReleaseScheduleInstruction(
obj.amount[i],
obj.commencementTimestamp[i],
obj.scheduleId[i],
cancelableBy,
tokenAccPubKey,
authAcc,
fromTokenAccount.address,
toTokenAccount,
nonceBytes
))

await this.transactionSizeControl(instructions, authAcc)
}

return new Transaction().add(...instructions)
}

async transactionSizeControl(instructions, feePayer) {
const transaction = new Transaction().add(...instructions)
transaction.feePayer = new PublicKey(feePayer)
transaction.recentBlockhash = (
await this.connection.getRecentBlockhash()
).blockhash

let msg = transaction.serializeMessage()
if (msg.length > SOLANA_MAX_TRANSACTION_UNSIGNED_SIZE) {
throw {
name: 'SolanaTransactionSizeError',
message: 'Transaction size too large. Please try to reduce transaction count.'
}
}
}

async createMint(tokenMintAddress, from) {
const mintPublicKey = new PublicKey(tokenMintAddress)

const mintToken = new Token(
this.connection,
mintPublicKey,
TOKEN_PROGRAM_ID,
new PublicKey(from)
)

return mintToken
}

async getAssociatedTokenAddress(mintToken, ownerPublicKey, payerPubKey, allowOwnerOffCurve = false) {
const associatedAccountTokenAddr = await Token.getAssociatedTokenAddress(
mintToken.associatedProgramId,
mintToken.programId,
mintToken.publicKey,
ownerPublicKey,
allowOwnerOffCurve
)
const receiverAccount = await this.connection.getAccountInfo(associatedAccountTokenAddr)

let instruction = null
if (receiverAccount === null) {
instruction =
Token.createAssociatedTokenAccountInstruction(
mintToken.associatedProgramId,
mintToken.programId,
mintToken.publicKey,
associatedAccountTokenAddr,
ownerPublicKey,
payerPubKey
)
}

return [associatedAccountTokenAddr, instruction]
}
}
26 changes: 26 additions & 0 deletions lib/TxValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const AlgorandTxValidator = require("./validators/AlgorandTxValidator.js").AlgorandTxValidator
const EthereumTxValidator = require("./validators/EthereumTxValidator").EthereumTxValidator
const SolanaTxValidator = require("./validators/SolanaTxValidator").SolanaTxValidator

class TxValidator {
constructor(params) {
if (params.type === "algorand") {
this.klass = new AlgorandTxValidator(params)
} else if (params.type === "ethereum") {
this.klass = new EthereumTxValidator(params)
} else if (params.type === "solana") {
this.klass = new SolanaTxValidator(params)
} else {
this.klass = undefined
}
}

async isTransactionValid(transaction, hotWalletAddress) {
return await this.klass.isTransactionValid(transaction, hotWalletAddress)
}
}

exports.TxValidator = TxValidator
exports.AlgorandTxValidator = AlgorandTxValidator
exports.EthereumTxValidator = EthereumTxValidator
exports.SolanaTxValidator = SolanaTxValidator
Loading