diff --git a/package.json b/package.json index efb0465037..e548acddd4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tatumio/tatum", - "version": "1.24.10", + "version": "1.25.0", "description": "Tatum API client allows browsers and Node.js clients to interact with Tatum API.", "main": "dist/src/index.js", "repository": "https://github.com/tatumio/tatum-js", diff --git a/src/model/request/AlgoTransaction.ts b/src/model/request/AlgoTransaction.ts new file mode 100644 index 0000000000..b45f9414da --- /dev/null +++ b/src/model/request/AlgoTransaction.ts @@ -0,0 +1,27 @@ +import { PrivateKeyOrSignatureId } from "./PrivateKeyOrSignatureId"; +import { IsNotEmpty, IsNumberString, IsOptional, Length, Matches, MaxLength, } from 'class-validator'; + +export class AlgoTransaction extends PrivateKeyOrSignatureId { + + @IsNotEmpty() + @Length(58, 58) + public from: string; + + @IsNotEmpty() + @Length(58, 58) + public to: string; + + @IsNotEmpty() + @IsNumberString() + @Matches(/^[+]?((\d+(\.\d*)?)|(\.\d+))$/) + public fee: string; + + @IsNotEmpty() + @IsNumberString() + @Matches(/^[+]?((\d+(\.\d*)?)|(\.\d+))$/) + public amount: string; + + @IsOptional() + @MaxLength(30) + public note: string; +} \ No newline at end of file diff --git a/src/model/request/index.ts b/src/model/request/index.ts index 765957c462..1c1580c0f5 100644 --- a/src/model/request/index.ts +++ b/src/model/request/index.ts @@ -142,3 +142,4 @@ export * from './UpdateCashbackErc721' export * from './PrivateKeyOrSignatureIdBtcBased' export {ContractType} from './ContractType' export * from './egld' +export * from './AlgoTransaction' \ No newline at end of file diff --git a/src/transaction/algo.spec.ts b/src/transaction/algo.spec.ts new file mode 100644 index 0000000000..cf93c87bcf --- /dev/null +++ b/src/transaction/algo.spec.ts @@ -0,0 +1,17 @@ +import { signAlgoTransaction } from './algo' +import { AlgoTransaction } from '../model'; + +describe('Algo transaction', () => { + jest.setTimeout(59999) + it('should test signed transaction for Algo transfer', async () => { + const tx = new AlgoTransaction(); + tx.from = 'TMETT6BXL3QUH7AH5TS6IONU7LVTLKIGG54CFCNPMQXWGRIZFIESZBYWP4'; + tx.to = 'NTAESFCB3WOD7SAOL42KSPVARLB3JFA3MNX3AESWHYVT2RMYDVZI6YLG4Y'; + tx.fee = '100'; + tx.amount = '1000000'; + tx.note = 'Helloworld'; + tx.fromPrivateKey = '72TCV5BRQPBMSAFPYO3CPWVDBYWNGAYNMTW5QHENOMQF7I6QLNMJWCJZ7A3V5YKD7QD6ZZPEHG2PV2ZVVEDDO6BCRGXWIL3DIUMSUCI'; + const txId = await signAlgoTransaction(true,tx, 'https://testnet-algorand.api.purestake.io/ps2'); + expect(txId.length).toBe(52) + }) +}) diff --git a/src/transaction/algo.ts b/src/transaction/algo.ts new file mode 100644 index 0000000000..a21990c10c --- /dev/null +++ b/src/transaction/algo.ts @@ -0,0 +1,72 @@ +const algosdk = require('algosdk'); +const base32 = require('base32.js'); +import { TextEncoder } from 'util'; +import { TATUM_API_URL } from '../constants'; +import { AlgoTransaction } from '../model'; + +/** + * + * @param testnet if the algorand node is testnet or not + * @param provider url of the algorand server endpoint for purestake.io restapi + * @returns algorand Client + */ +export const getClient = (testnet: boolean, provider?: string) => { + const baseServer = provider || `${process.env.TATUM_API_URL || TATUM_API_URL}/v3/algorand/node`; + const token = {'X-API-Key': `${process.env.ALGO_API_KEY}`} + const algodClient = new algosdk.Algodv2(token, baseServer, ''); + return algodClient; +} + +/** + * + * @param algodClient algorand Client + * @param txId transaction id + * @returns confirmed result + */ +const waitForConfirmation = async (algodClient: any, txId: string) => { + let lastround = (await algodClient.status().do())['last-round']; + let limit = 0; + while (limit < 50) { + const pendingInfo = await algodClient.pendingTransactionInformation(txId).do(); + if (pendingInfo['confirmed-round']) { + return true; + } else if (pendingInfo["pool-error"]) { + return false; + } + lastround++; + limit++; + await algodClient.statusAfterBlock(lastround).do(); + } + return false; +} + +/** + * Algorand transaction signing and confirm result + * @param testnet if the algorand node is testnet or not + * @param tx content of the transaction to broadcast + * @param provider url of the algorand server endpoint for purestake.io restapi + * @returns transaction id of the transaction in the blockchain + */ +export const signAlgoTransaction = async ( testnet: boolean, tx: AlgoTransaction, provider?: string) => { + const algodClient = getClient(testnet, provider); + const params = await algodClient.getTransactionParams().do(); + const decoder = new base32.Decoder({type: "rfc4648"}) + const secretKey = new Uint8Array(decoder.write(tx.fromPrivateKey).buf); + const enc = new TextEncoder(); + const note = enc.encode(tx.note ? tx.note : ''); + const txn = { + "from": tx.from, + "to": tx.to, + "fee": Number(tx.fee), + "amount": BigInt(tx.amount), + "firstRound": params.firstRound, + "lastRound": params.lastRound, + "genesisID": params.genesisID, + "genesisHash": params.genesisHash, + "note": note + } + const signedTxn = algosdk.signTransaction(txn, secretKey); + const sendTx = await algodClient.sendRawTransaction(signedTxn.blob).do(); + const confirm = await waitForConfirmation(algodClient, sendTx.txId); + if (confirm) return sendTx.txId; else throw Error("Failed Algo Transaction Signing"); +} diff --git a/src/wallet/address.ts b/src/wallet/address.ts index 51ede5f36f..f20f09b970 100644 --- a/src/wallet/address.ts +++ b/src/wallet/address.ts @@ -651,10 +651,9 @@ const convertXdcPrivateKey = (testnet: boolean, privKey: string) => { */ export const generateAlgodAddressFromPrivatetKey = (privKey: string) => { const decoder = new base32.Decoder({type: "rfc4648"}) - const scretKey = decoder.write(privKey).buf; - let mn = algosdk.secretKeyToMnemonic(scretKey) - const address = algosdk.mnemonicToSecretKey(mn).addr; - return address; + const secretKey = decoder.write(privKey).buf; + const mn = algosdk.secretKeyToMnemonic(secretKey) + return algosdk.mnemonicToSecretKey(mn).addr; }