diff --git a/src/config/index.ts b/src/config/index.ts index 4dc21e5..1321358 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -20,6 +20,7 @@ export default () => ({ }, arweave: { address: process.env.ARWEAVE_ADDRESS, + pk: process.env.ARWEAVE_PK, }, solana: { address: process.env.SOLANA_ADDRESS, @@ -34,6 +35,7 @@ export default () => ({ addresses: { evm: process.env.FEE_ADDRESS_EVM, solana: process.env.FEE_ADDRESS_SOLANA, + arweave: process.env.FEE_ADDRESS_ARWEAVE, }, feePercentage: process.env.FEE_PERCENTAGE, }, diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 365daf1..6d17f0e 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -168,9 +168,33 @@ export class AuthService { throw new BadRequestException('Invalid Arweave signature: nonce missing or mismatch'); } - const verified = this.getArweave().crypto.verify(publicKey, Uint8Array.from(Buffer.from(signedMessage)), Uint8Array.from(Buffer.from(signature))); + const publicJWK = { + e: "AQAB", + ext: true, + kty: "RSA", + n: publicKey + }; - if (!verified) { + const verificationKey = await crypto.subtle.importKey( + "jwk", + publicJWK, + { + name: "RSA-PSS", + hash: "SHA-256" + }, + false, + ["verify"] + ); + + const hash = await crypto.subtle.digest("SHA-256", Buffer.from(signedMessage)); + const isValidSignature = await crypto.subtle.verify( + { name: "RSA-PSS", saltLength: 32 }, + verificationKey, + Buffer.from(signature, 'base64'), + hash + ); + + if (!isValidSignature) { throw new BadRequestException('Invalid Arweave signature: verification failed'); } } diff --git a/src/modules/upload/upload.controller.ts b/src/modules/upload/upload.controller.ts index 5ba2a81..b242109 100644 --- a/src/modules/upload/upload.controller.ts +++ b/src/modules/upload/upload.controller.ts @@ -154,15 +154,25 @@ export class UploadController { throw new BadRequestException('Completed upload already exists with receipt id: ' + receipt.id); } - const verified = await this.uploadService.verifyPayment({ - paymentTx: txnHash, - chainType: token.chainType, - network: token.network, - chainId: +token.chainId, - senderAddress: (req as any).user.walletAddress, - amount: paymentTransaction.amountInSubUnits, - tokenAddress: token.address - }); + let verified = false; + let retries = 30; + while (!verified && retries > 0) { + verified = await this.uploadService.verifyPayment({ + paymentTx: txnHash, + chainType: token.chainType, + network: token.network, + chainId: +token.chainId, + senderAddress: (req as any).user.walletAddress, + amount: paymentTransaction.amountInSubUnits, + tokenAddress: token.address + }); + if (!verified) { + retries--; + if (retries > 0) { + await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 second between retries + } + } + } if (!verified) { throw new BadRequestException('Payment verification failed. Invalid transaction hash or amount'); } diff --git a/src/modules/upload/upload.service.ts b/src/modules/upload/upload.service.ts index 217bc26..c2636f2 100644 --- a/src/modules/upload/upload.service.ts +++ b/src/modules/upload/upload.service.ts @@ -2,6 +2,8 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ChainType, Network, ReceiptStatus, TokenTicker, TransactionStatus, UploadStatus, } from '@prisma/client'; import { Connection, ParsedInstruction } from '@solana/web3.js'; +import Arweave from 'arweave/node/common'; +import axios from 'axios'; import BigNumber from 'bignumber.js'; import { Contract, getAddress, Interface, Provider } from 'ethers'; import * as fs from 'fs'; @@ -110,8 +112,8 @@ export class UploadService { data: { userWalletAddress: user.walletAddress, tokenId: validToken.id, - amount: costInToken.amount, - amountInSubUnits: costInToken.amountInSubUnits, + amount: typeof costInToken.amount === 'string' ? costInToken.amount : BigNumber(costInToken.amount).toFixed(), + amountInSubUnits: typeof costInToken.amountInSubUnits === 'string' ? costInToken.amountInSubUnits : BigNumber(costInToken.amountInSubUnits).toString(), } }) @@ -122,7 +124,7 @@ export class UploadService { fileName: fileName, size: size, uploadType: uploadType, - uploadEstimate: costInToken.amount, + uploadEstimate: typeof costInToken.amount === 'string' ? costInToken.amount : BigNumber(costInToken.amount).toFixed(), uploadEstimateUSD: BigNumber(costEstimate.usd).toFixed(), mimeType: mimeType, totalChunks: totalChunks, @@ -375,6 +377,28 @@ export class UploadService { } } + if (chainType === ChainType.arweave) { + // + console.log({ paymentTx }) + const tx = await axios.post('https://arweave.net/graphql', { + query: `query{ transaction(id:"${paymentTx}"){ id, quantity{ winston ar }, owner{ address }, recipient } }`, + }); + const data = tx.data; + const transaction = data.data.transaction; + if (!transaction) { + return false; + } + console.log({ transaction }) + + const { quantity, owner, recipient } = transaction; + const quantityInWinston = quantity.winston; + const ownerAddress = owner.address; + const recipientAddress = recipient; + + const valid = BigInt(quantityInWinston) === BigInt(amount) && recipientAddress === systemAddress && ownerAddress === senderAddress; + return valid; + } + return false; } @@ -548,8 +572,29 @@ export class UploadService { throw new BadRequestException("Solana fee debit not implemented"); } + if (ChainType.arweave === token.chainType) { + const jwk = JSON.parse(systemPrivateKey) + + const arweave = Arweave.init({ + host: 'arweave.net', + port: 443, + protocol: 'https', + }) + console.log({ feeAmount }) + const txn = await arweave.createTransaction({ quantity: feeAmount, target: feeAddress }) + await arweave.transactions.sign(txn, jwk) + const res = await arweave.transactions.post(txn) + console.log({ res }) + + if (res.status !== 200) { + throw new BadRequestException("Fee transaction failed"); + } - + await this.databaseService.feeTransaction.update({ + where: { id: feeTransactionId }, + data: { status: TransactionStatus.SUCCEEDED, transactionHash: txn.id } + }); + } } private async validateToken(chainType: ChainType, tokenTicker: TokenTicker, chainId: number, network: Network) { diff --git a/src/modules/upload/utils/Web3Provider.util.ts b/src/modules/upload/utils/Web3Provider.util.ts index 343eebd..69fcc38 100644 --- a/src/modules/upload/utils/Web3Provider.util.ts +++ b/src/modules/upload/utils/Web3Provider.util.ts @@ -1,6 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { ChainType, Network } from '@prisma/client'; import { Connection } from '@solana/web3.js'; +import Arweave from "arweave/node"; import { ethers, Provider, Signer } from 'ethers'; @@ -57,11 +58,17 @@ export class Web3Provider { * @param chainType - The type of blockchain (EVM or SOLANA). * @param network - The network to connect to (MAINNET or TESTNET). */ - public static getProvider(chainType: ChainType, network: Network, chainId: number): Provider | Connection { + public static getProvider(chainType: ChainType, network: Network, chainId: number): Provider | Connection | Arweave { if (chainType === ChainType.evm) { return this.getEvmProvider(chainId); } else if (chainType === ChainType.solana) { return this.getSolanaConnection(network); + } else if (chainType === ChainType.arweave) { + return Arweave.init({ + host: 'arweave.net', + port: 443, + protocol: 'https', + }); } else { throw new Error('Unsupported chain type'); } @@ -71,8 +78,8 @@ export class Web3Provider { if (chainType === ChainType.evm) { return new ethers.Wallet(privateKey, this.getEvmProvider(chainId)); } else if (chainType === ChainType.solana) { - // TODO: Implement solana signer - throw new BadRequestException("Solana signer not implemented"); + // TODO: Implement solana signer + throw new BadRequestException("Solana signer not implemented"); } else { throw new Error('Unsupported chain type'); }