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

Feat/support xrpl check #32

Merged
merged 4 commits into from
Oct 5, 2024
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# Local Netlify folder
.netlify
256 changes: 228 additions & 28 deletions src/network-handlers/ripple-handler.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import { Decimal } from 'decimal.js';
import { BigNumber } from 'ethers';
import xrpl, { AccountNFTsRequest, SubmittableTransaction } from 'xrpl';
import xrpl, {
AccountNFTsRequest,
AccountObject,
AccountObjectsResponse,
CheckCash,
CheckCreate,
IssuedCurrencyAmount,
LedgerEntry,
Payment,
Request,
SubmittableTransaction,
TxResponse,
} from 'xrpl';
import { NFTokenMintMetadata } from 'xrpl/dist/npm/models/transactions/NFTokenMint.js';

import { RippleError } from '../models/errors.js';
import { RawVault, VaultState } from '../models/ethereum-models.js';
import { shiftValue, unshiftValue } from '../utilities/index.js';

interface SignResponse {
tx_blob: string;
hash: string;
}

function encodeNftURI(vault: RawVault): string {
const VERSION = parseInt('1', 16).toString().padStart(2, '0'); // 1 as hex
const status = parseInt(vault.status.toString(), 16).toString().padStart(2, '0');
console.log(
`UUID: ${vault.uuid}, valueLocked: ${vault.valueLocked}, valueMinted: ${vault.valueMinted}`
);
// console.log(
// `UUID: ${vault.uuid}, valueLocked: ${vault.valueLocked}, valueMinted: ${vault.valueMinted}`
// );
let uuid = vault.uuid;
if (uuid === '') {
uuid = vault.uuid.padStart(64, '0');
Expand All @@ -25,7 +44,7 @@
const wdTxId = vault.wdTxId.padStart(64, '0');
const btcMintFeeBasisPoints = vault.btcMintFeeBasisPoints._hex.substring(2).padStart(2, '0');
const btcRedeemFeeBasisPoints = vault.btcRedeemFeeBasisPoints._hex.substring(2).padStart(2, '0');
const btcFeeRecipient = vault.btcFeeRecipient.padStart(64, '0');
const btcFeeRecipient = vault.btcFeeRecipient.padStart(66, '0');
const taprootPubKey = vault.taprootPubKey.padStart(64, '0');
console.log(
'built URI:',
Expand All @@ -47,7 +66,7 @@
let btcFeeRecipient = '';
let taprootPubKey = '';
try {
VERSION = parseInt(URI.slice(0, 2), 16);

Check failure on line 69 in src/network-handlers/ripple-handler.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

'VERSION' is assigned a value but never used
status = parseInt(URI.slice(2, 4), 16);
uuid = URI.slice(4, 68);
valueLocked = BigNumber.from(`0x${URI.slice(68, 84)}`);
Expand Down Expand Up @@ -79,6 +98,7 @@
status: status,
valueLocked: valueLocked,
valueMinted: valueMinted,
creator: 'rfvtbrXSxLsxVWDktR4sdzjJgv8EnMKFKG',
fundingTxId: fundingTxId,
wdTxId: wdTxId,
btcMintFeeBasisPoints: btcMintFeeBasisPoints,
Expand Down Expand Up @@ -106,7 +126,7 @@
valueMinted: BigNumber.from(0),
protocolContract: '',
timestamp: BigNumber.from(0),
creator: '',
creator: 'rfvtbrXSxLsxVWDktR4sdzjJgv8EnMKFKG',
status: 0,
fundingTxId: '0'.repeat(64),
closingTxId: '',
Expand All @@ -120,11 +140,13 @@

export class RippleHandler {
private client: xrpl.Client;
private demo_wallet: xrpl.Wallet;
private customerWallet: xrpl.Wallet;
private issuerWallet: xrpl.Wallet;

private constructor() {
this.client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');
this.demo_wallet = xrpl.Wallet.fromSeed('sEdV6wSeVoUGwu7KHyFZ3UkrQxpvGZU'); //rNT2CxBbKtiUwy4UFwXS11PETZVW8j4k3g
this.issuerWallet = xrpl.Wallet.fromSeed('sEdVJZsxTCVMCvLzUHkXsdDPrvtj8Lo');
this.customerWallet = xrpl.Wallet.fromSeed('sEdSKUhR1Hhwomo7CsUzAe2pv7nqUXT');
}

static fromWhatever(): RippleHandler {
Expand All @@ -147,7 +169,7 @@
await this.client.connect();
}
try {
return this.demo_wallet.classicAddress;
return this.customerWallet.classicAddress;
} catch (error) {
throw new RippleError(`Could not fetch Address Info: ${error}`);
}
Expand Down Expand Up @@ -192,17 +214,19 @@
try {
const getNFTsTransaction: AccountNFTsRequest = {
command: 'account_nfts',
account: this.demo_wallet.classicAddress,
account: this.issuerWallet.classicAddress,
};
let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid;
nftUUID = nftUUID.toUpperCase();
const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction);
const nftTokenId = await this.getNFTokenIdForVault(nftUUID);
const matchingNFT = nfts.result.account_nfts.find(nft => nft.NFTokenID === nftTokenId);
if (!matchingNFT) {
const matchingNFT = nfts.result.account_nfts.filter(nft => nft.NFTokenID === nftTokenId);
if (matchingNFT.length === 0) {
throw new RippleError(`Vault with UUID: ${nftUUID} not found`);
} else if (matchingNFT.length > 1) {
throw new RippleError(`Multiple Vaults with UUID: ${nftUUID} found`);
}
const matchingVault: RawVault = decodeNftURI(matchingNFT.URI!);
const matchingVault: RawVault = decodeNftURI(matchingNFT[0].URI!);
return lowercaseHexFields(matchingVault);
} catch (error) {
throw new RippleError(`Could not fetch Vault: ${error}`);
Expand Down Expand Up @@ -236,11 +260,12 @@
await this.client.connect();
}
try {
console.log(`Performing Withdraw for User: ${uuid}`);
let nftUUID = uuid.substring(0, 2) === '0x' ? uuid.slice(2) : uuid;
nftUUID = nftUUID.toUpperCase();
// return await withdraw(this.ethereumContracts.dlcManagerContract, vaultUUID, withdrawAmount);
const thisVault = await this.getRawVault(nftUUID);
await this.burnNFT(nftUUID);

thisVault.valueMinted = thisVault.valueMinted.sub(BigNumber.from(withdrawAmount));
await this.mintNFT(thisVault);
} catch (error) {
Expand Down Expand Up @@ -271,6 +296,13 @@
valueMinted: BigNumber.from(updatedValueMinted),
valueLocked: BigNumber.from(updatedValueMinted),
};
if (updatedValueMinted > 0 && thisVault.valueMinted.toNumber() < Number(updatedValueMinted)) {
const mintValue = unshiftValue(
new Decimal(Number(updatedValueMinted)).minus(thisVault.valueMinted.toNumber()).toNumber()
);
console.log(`Minting ${mintValue}`);
await this.mintToken(thisVault.creator, mintValue.toString());
}
await this.mintNFT(newVault);
console.log(`Vault status set to FUNDED, vault: ${nftUUID}`);
} catch (error) {
Expand Down Expand Up @@ -367,7 +399,7 @@
try {
const getNFTsTransaction: AccountNFTsRequest = {
command: 'account_nfts',
account: this.demo_wallet.classicAddress,
account: this.issuerWallet.classicAddress,
};

const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction);
Expand All @@ -394,7 +426,7 @@
try {
const getNFTsTransaction: AccountNFTsRequest = {
command: 'account_nfts',
account: this.demo_wallet.classicAddress,
account: this.issuerWallet.classicAddress,
};

const nfts: xrpl.AccountNFTsResponse = await this.client.request(getNFTsTransaction);
Expand Down Expand Up @@ -428,13 +460,15 @@
const nftTokenId = await this.getNFTokenIdForVault(nftUUID);
const burnTransactionJson: SubmittableTransaction = {
TransactionType: 'NFTokenBurn',
Account: this.demo_wallet.classicAddress,
Account: this.issuerWallet.classicAddress,
NFTokenID: nftTokenId,
};
const burnTx: xrpl.TxResponse<xrpl.SubmittableTransaction> = await this.client.submitAndWait(
burnTransactionJson,
{ wallet: this.demo_wallet }
{ wallet: this.issuerWallet }
);

console.log('burnTx:', burnTx);
const burnMeta: NFTokenMintMetadata = burnTx.result.meta! as NFTokenMintMetadata;
if (burnMeta!.TransactionResult !== 'tesSUCCESS') {
throw new RippleError(
Expand All @@ -447,22 +481,188 @@
if (!this.client.isConnected()) {
await this.client.connect();
}
console.log(`Minting Ripple Vault, vault: ${JSON.stringify(vault, null, 2)}`);
console.log(`Minting NFT with properties of Vault ${vault.uuid}`);

const newURI = encodeNftURI(vault);
const mintTransactionJson: SubmittableTransaction = {

const mintNFTTransactionJSON: SubmittableTransaction = {
TransactionType: 'NFTokenMint',
Account: this.demo_wallet.classicAddress,
Account: this.issuerWallet.classicAddress,
URI: newURI,
NFTokenTaxon: 0,
};
const mintTx: xrpl.TxResponse<xrpl.SubmittableTransaction> = await this.client.submitAndWait(
mintTransactionJson,
{ wallet: this.demo_wallet }

const mintNFTTransactionResponse: xrpl.TxResponse<xrpl.SubmittableTransaction> =
await this.client.submitAndWait(mintNFTTransactionJSON, { wallet: this.issuerWallet });

const mintNFTTransactionResponseMetadata = mintNFTTransactionResponse.result
.meta as NFTokenMintMetadata;

if (mintNFTTransactionResponseMetadata!.TransactionResult !== 'tesSUCCESS') {
throw new RippleError(
`Could not mint NFT: ${mintNFTTransactionResponseMetadata.TransactionResult}`
);
}

if (!mintNFTTransactionResponseMetadata.nftoken_id) {
throw new RippleError('Could not find NFT Token ID in NFTokenMint response metadata');
}

return mintNFTTransactionResponseMetadata.nftoken_id;
}

async getAllChecks(): Promise<AccountObject[]> {
if (!this.client.isConnected()) {
await this.client.connect();
}

const getAccountObjectsRequestJSON: Request = {
command: 'account_objects',
account: this.issuerWallet.classicAddress,
ledger_index: 'validated',
type: 'check',
};

const getAccountObjectsResponse: AccountObjectsResponse = await this.client.request(
getAccountObjectsRequestJSON
);

return getAccountObjectsResponse.result.account_objects;
}

async getAndCashAllChecksAndUpdateNFT(): Promise<void> {
const allChecks = (await this.getAllChecks()) as LedgerEntry.Check[];
console.log('All Checks:', allChecks);
const allVaults = await this.getContractVaults();

for (const check of allChecks) {
try {
const checkSendMax = check.SendMax as IssuedCurrencyAmount;

await this.cashCheck(check.index, checkSendMax.value);

const vault = allVaults.find(
vault => vault.uuid.toUpperCase().slice(2) === check.InvoiceID
);
if (!vault) {
throw new RippleError(
`Could not find Vault for Check with Invoice ID: ${check.InvoiceID}`
);
}
await this.withdraw(vault.uuid, BigInt(shiftValue(Number(checkSendMax.value))));
} catch (error) {
console.error(`Error cashing Check: ${error}`);
}
}
}

async createCheck(dlcBTCAmount: string, vaultUUID: string): Promise<string> {
if (!this.client.isConnected()) {
await this.client.connect();
}
console.log(`Creating Check for Vault ${vaultUUID} with an amount of ${dlcBTCAmount}`);

const createCheckTransactionJSON: CheckCreate = {
TransactionType: 'CheckCreate',
Account: this.customerWallet.classicAddress,
Destination: this.issuerWallet.classicAddress,
DestinationTag: 1,
SendMax: {
currency: 'DLC',
value: unshiftValue(Number(dlcBTCAmount)).toString(),
issuer: this.issuerWallet.classicAddress,
},
InvoiceID: vaultUUID.slice(2),
};

const updatedCreateCheckTransactionJSON: CheckCreate = await this.client.autofill(
createCheckTransactionJSON
);

const signCreateCheckTransactionResponse: SignResponse = this.customerWallet.sign(
updatedCreateCheckTransactionJSON
);

const submitCreateCheckTransactionResponse: TxResponse<SubmittableTransaction> =
await this.client.submitAndWait(signCreateCheckTransactionResponse.tx_blob);

console.log(
`Response for submitted Create Check for Vault ${vaultUUID} request: ${JSON.stringify(submitCreateCheckTransactionResponse, null, 2)}`
);
const meta: NFTokenMintMetadata = mintTx.result.meta! as NFTokenMintMetadata;
if (meta!.TransactionResult !== 'tesSUCCESS') {
throw new RippleError(`Could not mint Ripple Vault: ${meta!.TransactionResult}`);

return submitCreateCheckTransactionResponse.result.hash;
}

async cashCheck(checkID: string, dlcBTCAmount: string): Promise<string> {
if (!this.client.isConnected()) {
await this.client.connect();
}
return meta!.nftoken_id!;
if (checkID === '8FC923A16C90FB7316673D35CA228C82916B8E9F63EADC57BAA7C51C2E7716AA')
throw new Error('Invalid Check');

console.log(`Cashing Check of Check ID ${checkID} for an amount of ${dlcBTCAmount}`);

const cashCheckTransactionJSON: CheckCash = {
TransactionType: 'CheckCash',
Account: this.issuerWallet.classicAddress,
CheckID: checkID,
Amount: {
currency: 'DLC',
value: dlcBTCAmount,
issuer: this.issuerWallet.classicAddress,
},
};

const updatedCashCheckTransactionJSON: CheckCash =
await this.client.autofill(cashCheckTransactionJSON);

const signCashCheckTransactionResponse: SignResponse = this.issuerWallet.sign(
updatedCashCheckTransactionJSON
);

const submitCashCheckTransactionResponse: TxResponse<SubmittableTransaction> =
await this.client.submitAndWait(signCashCheckTransactionResponse.tx_blob);

console.log(
`Response for submitted Cash Check of Check ID ${checkID} request: ${JSON.stringify(submitCashCheckTransactionResponse, null, 2)}`
);

return submitCashCheckTransactionResponse.result.hash;
}

async mintToken(xrplDestinationAddress: string, dlcBTCAmount: string): Promise<string> {
if (!this.client.isConnected()) {
await this.client.connect();
}

console.log(`Minting ${dlcBTCAmount} dlcBTC to ${xrplDestinationAddress} address`);

const sendTokenTransactionJSON: Payment = {
TransactionType: 'Payment',
Account: this.issuerWallet.classicAddress,
Destination: xrplDestinationAddress,
DestinationTag: 1,
Amount: {
currency: 'DLC',
value: dlcBTCAmount,
issuer: this.issuerWallet.classicAddress,
},
};

const updatedSendTokenTransactionJSON: Payment =
await this.client.autofill(sendTokenTransactionJSON);

const signSendTokenTransactionResponse: SignResponse = this.issuerWallet.sign(
updatedSendTokenTransactionJSON
);

const submitSendTokenTransactionResponse: TxResponse<SubmittableTransaction> =
await this.client.submitAndWait(signSendTokenTransactionResponse.tx_blob);

console.log(
`Response for submitted Payment to ${xrplDestinationAddress} address request: ${JSON.stringify(submitSendTokenTransactionResponse, null, 2)}`
);

return submitSendTokenTransactionResponse.result.hash;
}
}
Loading