mirror of
https://github.com/ChanX21/Sigillum
synced 2026-04-21 23:57:26 +00:00
1204 lines
31 KiB
TypeScript
1204 lines
31 KiB
TypeScript
// CONSTANTS
|
|
import { Transaction } from "@mysten/sui/transactions";
|
|
|
|
import { EventId, SuiClient, SuiEvent } from "@mysten/sui/client";
|
|
import { MODULE_NAME, PACKAGE_ID } from "@/lib/suiConfig";
|
|
const nftTypeArg = `${PACKAGE_ID}::sigillum_nft::PhotoNFT`;
|
|
export const buildAcceptBidTx = (
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
packageId: string,
|
|
moduleName: string
|
|
): Transaction => {
|
|
const tx = new Transaction();
|
|
|
|
// Set reasonable gas budget
|
|
const estimatedGasFee = BigInt(30000000); // 0.03 SUI
|
|
tx.setGasBudget(Number(estimatedGasFee));
|
|
|
|
// Building the move call with type arguments
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::accept_bid`,
|
|
typeArguments: [nftTypeArg], // Add this line with the NFT type
|
|
arguments: [tx.object(marketplaceObjectId), tx.pure.address(listingId)],
|
|
});
|
|
|
|
return tx;
|
|
};
|
|
export const buildWithdrawStakeTx = (
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
packageId: string,
|
|
moduleName: string
|
|
): Transaction => {
|
|
const tx = new Transaction();
|
|
|
|
// Set reasonable gas budget
|
|
const estimatedGasFee = BigInt(30000000); // 0.03 SUI
|
|
tx.setGasBudget(Number(estimatedGasFee));
|
|
|
|
// Building the move call with type arguments
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::withdraw_stake`,
|
|
arguments: [tx.object(marketplaceObjectId), tx.pure.address(listingId)],
|
|
});
|
|
|
|
return tx;
|
|
};
|
|
|
|
function bytesToHex(bytes: number[]): string {
|
|
return "0x" + bytes.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
// Function to get listing details
|
|
export async function getObjectDetails(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
address: string | null = null
|
|
) {
|
|
if (!address) return null;
|
|
// If no address is provided, use a default address for read-only operations
|
|
const senderAddress = address;
|
|
|
|
try {
|
|
const tx = new Transaction();
|
|
|
|
// Call the function
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::get_listing_details`,
|
|
arguments: [tx.object(marketplaceObjectId), tx.pure.address(listingId)],
|
|
});
|
|
|
|
const result = await provider.devInspectTransactionBlock({
|
|
sender: senderAddress,
|
|
transactionBlock: tx,
|
|
});
|
|
// console.log("Result:", result);
|
|
|
|
//Check for dynamic_field error
|
|
if (
|
|
result.error &&
|
|
(result.error.includes("dynamic_field") ||
|
|
result.error.includes("MoveAbort"))
|
|
) {
|
|
console.log("Listing not found or not accessible.", result.error);
|
|
return null;
|
|
}
|
|
if (
|
|
result &&
|
|
result.results &&
|
|
result.results[0] &&
|
|
result.results[0].returnValues &&
|
|
result.results[0].returnValues.length > 10
|
|
) {
|
|
const returnValues = result.results[0].returnValues;
|
|
|
|
const val = {
|
|
owner: bytesToHex([...new Uint8Array(returnValues[0][0])]),
|
|
nftId: bytesToHex([...new Uint8Array(returnValues[1][0])]),
|
|
listPrice: BigInt(
|
|
new DataView(Uint8Array.from(returnValues[2][0]).buffer).getBigUint64(
|
|
0,
|
|
true
|
|
)
|
|
),
|
|
listingType: returnValues[3][0][0],
|
|
minBid: BigInt(
|
|
new DataView(Uint8Array.from(returnValues[4][0]).buffer).getBigUint64(
|
|
0,
|
|
true
|
|
)
|
|
),
|
|
highestBid: BigInt(
|
|
new DataView(Uint8Array.from(returnValues[5][0]).buffer).getBigUint64(
|
|
0,
|
|
true
|
|
)
|
|
),
|
|
highestBidder: bytesToHex([...new Uint8Array(returnValues[6][0])]),
|
|
active: Boolean(returnValues[7][0][0]),
|
|
verificationScore: BigInt(
|
|
new DataView(Uint8Array.from(returnValues[8][0]).buffer).getBigUint64(
|
|
0,
|
|
true
|
|
)
|
|
),
|
|
startTime: BigInt(
|
|
new DataView(Uint8Array.from(returnValues[9][0]).buffer).getBigUint64(
|
|
0,
|
|
true
|
|
)
|
|
),
|
|
endTime: BigInt(
|
|
new DataView(
|
|
Uint8Array.from(returnValues[10][0]).buffer
|
|
).getBigUint64(0, true)
|
|
),
|
|
};
|
|
|
|
return val;
|
|
} else {
|
|
console.error("Invalid result structure:", result);
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in getListingDetails:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Helper function to parse a u64 from a byte array
|
|
function parseU64FromByteArray(byteArray: number[]): number {
|
|
if (!Array.isArray(byteArray) || byteArray.length !== 8) {
|
|
throw new Error(`Invalid u64 byte array: ${JSON.stringify(byteArray)}`);
|
|
}
|
|
|
|
// Little-endian conversion of byte array to number (least significant byte first)
|
|
let value = BigInt(0);
|
|
for (let i = 7; i >= 0; i--) {
|
|
value = (value << BigInt(8)) | BigInt(byteArray[i]);
|
|
}
|
|
|
|
// Convert to Number if within safe integer range
|
|
if (value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
return Number(value);
|
|
}
|
|
|
|
// Otherwise return as number (might lose precision for very large values)
|
|
return Number(value);
|
|
}
|
|
|
|
// Generic function to call Move functions that return a u64
|
|
async function callMoveWithU64Return(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
functionName: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
address: string | null = null,
|
|
errorPrefix: string = "Failed to get value"
|
|
): Promise<number | null> {
|
|
if (!address) return null;
|
|
const tx = new Transaction();
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::${functionName}`,
|
|
arguments: [tx.object(marketplaceObjectId), tx.pure.address(listingId)],
|
|
});
|
|
|
|
try {
|
|
const result = await provider.devInspectTransactionBlock({
|
|
sender: address,
|
|
transactionBlock: tx,
|
|
});
|
|
|
|
// Check if we have valid results
|
|
if (
|
|
!result?.results?.[0]?.returnValues ||
|
|
!Array.isArray(result.results[0].returnValues)
|
|
) {
|
|
throw new Error("Unexpected result format");
|
|
}
|
|
|
|
// The byte array is the first and only return value
|
|
const returnValues = result.results[0].returnValues;
|
|
|
|
// Based on the example, the u64 value is the first item in returnValues
|
|
if (returnValues[0] && returnValues[0][1] === "u64") {
|
|
return parseU64FromByteArray(returnValues[0][0]);
|
|
}
|
|
|
|
// Fallback: search for any u64 value
|
|
for (const [value, type] of returnValues) {
|
|
if (type === "u64") {
|
|
return parseU64FromByteArray(value);
|
|
}
|
|
}
|
|
|
|
throw new Error(`Could not find u64 value in response for ${functionName}`);
|
|
} catch (error: unknown) {
|
|
console.error(`Error calling ${functionName}:`, error);
|
|
throw new Error(
|
|
`${errorPrefix}: ${
|
|
error instanceof Error ? error.message : "Unknown error occurred"
|
|
}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Specific implementation for stakers count
|
|
export async function getStakersCount(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
address: string | null = null
|
|
) {
|
|
return callMoveWithU64Return(
|
|
provider,
|
|
packageId,
|
|
moduleName,
|
|
"get_stakers_count",
|
|
marketplaceObjectId,
|
|
listingId,
|
|
address,
|
|
"Failed to get stakers count"
|
|
);
|
|
}
|
|
|
|
// Specific implementation for bid count
|
|
export async function getBidCount(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
address: string | null = null
|
|
) {
|
|
return callMoveWithU64Return(
|
|
provider,
|
|
packageId,
|
|
moduleName,
|
|
"get_bid_count",
|
|
marketplaceObjectId,
|
|
listingId,
|
|
address,
|
|
"Failed to get bid count"
|
|
);
|
|
}
|
|
// Function to get fee percentage
|
|
export async function getFeePercentage(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
address: string | null = null
|
|
) {
|
|
if (!address) return null;
|
|
const tx = new Transaction();
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::get_fee_percentage`,
|
|
arguments: [tx.object(marketplaceObjectId)],
|
|
});
|
|
|
|
const result = await provider.devInspectTransactionBlock({
|
|
sender: address,
|
|
transactionBlock: tx,
|
|
});
|
|
|
|
if (
|
|
result &&
|
|
result.results &&
|
|
result.results[0] &&
|
|
result.results[0].returnValues
|
|
) {
|
|
return Number(result.results[0].returnValues[0][0]);
|
|
}
|
|
|
|
throw new Error("Failed to get fee percentage");
|
|
}
|
|
|
|
// Function to get total volume
|
|
export async function getTotalVolume(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
address: string | null = null
|
|
) {
|
|
if (!address) return null;
|
|
const tx = new Transaction();
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::get_total_volume`,
|
|
arguments: [tx.object(marketplaceObjectId)],
|
|
});
|
|
|
|
const result = await provider.devInspectTransactionBlock({
|
|
sender: address,
|
|
transactionBlock: tx,
|
|
});
|
|
|
|
if (
|
|
result &&
|
|
result.results &&
|
|
result.results[0] &&
|
|
result.results[0].returnValues
|
|
) {
|
|
return Number(result.results[0].returnValues[0][0]);
|
|
}
|
|
|
|
throw new Error("Failed to get total volume");
|
|
}
|
|
|
|
// Function to get total listings
|
|
export async function getTotalListings(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
address: string | null = null
|
|
) {
|
|
if (!address) return null;
|
|
const tx = new Transaction();
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::get_total_listings`,
|
|
arguments: [tx.object(marketplaceObjectId)],
|
|
});
|
|
|
|
const result = await provider.devInspectTransactionBlock({
|
|
sender: address,
|
|
transactionBlock: tx,
|
|
});
|
|
|
|
if (
|
|
result &&
|
|
result.results &&
|
|
result.results[0] &&
|
|
result.results[0].returnValues
|
|
) {
|
|
return Number(result.results[0].returnValues[0][0]);
|
|
}
|
|
|
|
throw new Error("Failed to get total listings");
|
|
}
|
|
|
|
// Function to get listing IDs with pagination and filtering
|
|
export async function getListingIds(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
startIdx: number = 0,
|
|
limit: number = 10,
|
|
onlyActive: boolean = true,
|
|
listingType: number = 0,
|
|
address: string | null = null
|
|
): Promise<{ listingIds: string[]; hasMore: boolean }> {
|
|
if (!address) return { listingIds: [], hasMore: false };
|
|
|
|
const tx = new Transaction();
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::get_listing_ids`,
|
|
arguments: [
|
|
tx.object(marketplaceObjectId),
|
|
tx.pure.u64(startIdx),
|
|
tx.pure.u64(limit),
|
|
tx.pure.bool(onlyActive),
|
|
tx.pure.u8(listingType),
|
|
],
|
|
});
|
|
|
|
const result = await provider.devInspectTransactionBlock({
|
|
sender: address,
|
|
transactionBlock: tx,
|
|
});
|
|
|
|
if (
|
|
result &&
|
|
result.results &&
|
|
result.results[0] &&
|
|
result.results[0].returnValues
|
|
) {
|
|
const returnValues = result.results[0].returnValues;
|
|
|
|
// Parse the vector of addresses (first return value)
|
|
const listingIds = Array.isArray(returnValues[0][0])
|
|
? returnValues[0][0].map((id: any) => String(id))
|
|
: [];
|
|
|
|
// Parse the boolean hasMore flag (second return value)
|
|
const hasMore = Boolean(returnValues[1][0]);
|
|
|
|
return {
|
|
listingIds,
|
|
hasMore,
|
|
};
|
|
}
|
|
|
|
throw new Error("Failed to get listing IDs");
|
|
}
|
|
|
|
export const buildPlaceBidTx = (
|
|
marketplaceObjectId: string, // ID of the marketplace object
|
|
listingId: string, // listing_id
|
|
coinObjectId: string, // ID of the Coin<SUI> object to use for payment
|
|
packageId: string,
|
|
moduleName: string,
|
|
bidAmountMist: bigint, // The amount to bid
|
|
address: string // User's address
|
|
): Transaction => {
|
|
const tx = new Transaction();
|
|
|
|
// Set reasonable gas budget
|
|
const estimatedGasFee = BigInt(30000000); // 0.03 SUI
|
|
tx.setGasBudget(Number(estimatedGasFee));
|
|
|
|
// Split the coin
|
|
const bidCoin = tx.splitCoins(tx.object(coinObjectId), [
|
|
tx.pure.u64(bidAmountMist.toString()),
|
|
]);
|
|
|
|
// Mapping the arguments
|
|
const marketplaceArg = tx.object(marketplaceObjectId); // Shared marketplace object
|
|
const listingIdArg = tx.pure.address(listingId); // listing_id as address
|
|
const paymentArg = bidCoin[0]; // Coin<SUI> object ID
|
|
|
|
// Building the move call
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::place_bid`,
|
|
arguments: [marketplaceArg, listingIdArg, paymentArg],
|
|
});
|
|
|
|
// Return remaining coin to the user
|
|
// tx.transferObjects([remainingCoin], tx.pure.address(address));
|
|
|
|
return tx;
|
|
};
|
|
|
|
export async function buildPrepareCoinsTx(
|
|
provider: SuiClient,
|
|
address: string,
|
|
estimatedGasFee: bigint = BigInt(30_000_000)
|
|
): Promise<{ transaction: Transaction; success: boolean; reason?: string }> {
|
|
const { data: coinData } = await provider.getCoins({
|
|
owner: address,
|
|
coinType: "0x2::sui::SUI",
|
|
});
|
|
|
|
if (!coinData || coinData.length === 0) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
reason: "No coins found",
|
|
};
|
|
}
|
|
|
|
const sorted = [...coinData].sort((a, b) =>
|
|
Number(BigInt(b.balance) - BigInt(a.balance))
|
|
);
|
|
|
|
const mainCoin = sorted[0];
|
|
if (BigInt(mainCoin.balance) < estimatedGasFee * BigInt(2)) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
reason: "Not enough balance to split a second coin for gas.",
|
|
};
|
|
}
|
|
|
|
const tx = new Transaction();
|
|
const coinObj = tx.object(mainCoin.coinObjectId);
|
|
const splitCoin = tx.splitCoins(coinObj, [
|
|
tx.pure.u64(estimatedGasFee.toString()),
|
|
]);
|
|
tx.transferObjects([splitCoin[0]], tx.pure.address(address)); // Transfer to self (creates new coin object)
|
|
|
|
return { transaction: tx, success: true };
|
|
}
|
|
|
|
export async function buildPlaceBidTxWithCoinSelection(
|
|
provider: SuiClient,
|
|
address: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
bidAmountMist: bigint,
|
|
packageId: string,
|
|
moduleName: string
|
|
): Promise<{
|
|
transaction: Transaction;
|
|
success: boolean;
|
|
needsPreparation?: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const { data: coinData } = await provider.getCoins({
|
|
owner: address,
|
|
coinType: "0x2::sui::SUI",
|
|
});
|
|
|
|
if (!coinData || coinData.length === 0) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
error: "No coins available",
|
|
};
|
|
}
|
|
|
|
const sortedCoins = [...coinData].sort((a, b) =>
|
|
Number(BigInt(b.balance) - BigInt(a.balance))
|
|
);
|
|
|
|
const estimatedGasFee = BigInt(30_000_000); // 0.03 SUI
|
|
|
|
const totalBalance = sortedCoins.reduce(
|
|
(sum, coin) => sum + BigInt(coin.balance),
|
|
BigInt(0)
|
|
);
|
|
|
|
if (totalBalance < bidAmountMist + estimatedGasFee) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
error: `Insufficient balance. Need ${
|
|
Number(bidAmountMist + estimatedGasFee) / 1_000_000_000
|
|
} SUI, but have ${Number(totalBalance) / 1_000_000_000} SUI.`,
|
|
};
|
|
}
|
|
|
|
let gasCoin = null;
|
|
let bidCoin = null;
|
|
|
|
for (const coin of sortedCoins) {
|
|
if (
|
|
BigInt(coin.balance) >= estimatedGasFee &&
|
|
BigInt(coin.balance) < bidAmountMist + estimatedGasFee
|
|
) {
|
|
gasCoin = coin;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!gasCoin) {
|
|
for (const coin of sortedCoins) {
|
|
if (BigInt(coin.balance) >= estimatedGasFee) {
|
|
gasCoin = coin;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (gasCoin) {
|
|
for (const coin of sortedCoins) {
|
|
if (
|
|
coin.coinObjectId !== gasCoin.coinObjectId &&
|
|
BigInt(coin.balance) >= bidAmountMist
|
|
) {
|
|
bidCoin = coin;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!gasCoin || !bidCoin) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
needsPreparation: true,
|
|
error:
|
|
"Need to split coin first. Cannot use same coin for gas and bid.",
|
|
};
|
|
}
|
|
|
|
const tx = new Transaction();
|
|
tx.setSender(address);
|
|
tx.setGasBudget(Number(estimatedGasFee));
|
|
tx.setGasPayment([
|
|
{
|
|
objectId: gasCoin.coinObjectId,
|
|
version: gasCoin.version,
|
|
digest: gasCoin.digest,
|
|
},
|
|
]);
|
|
|
|
const bidCoinObj = tx.object(bidCoin.coinObjectId);
|
|
|
|
if (BigInt(bidCoin.balance) > bidAmountMist) {
|
|
const splitBidCoins = tx.splitCoins(bidCoinObj, [
|
|
tx.pure.u64(bidAmountMist.toString()),
|
|
]);
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::place_bid`,
|
|
arguments: [
|
|
tx.object(marketplaceObjectId),
|
|
tx.pure.address(listingId),
|
|
splitBidCoins[0],
|
|
],
|
|
});
|
|
} else {
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::place_bid`,
|
|
arguments: [
|
|
tx.object(marketplaceObjectId),
|
|
tx.pure.address(listingId),
|
|
bidCoinObj,
|
|
],
|
|
});
|
|
}
|
|
|
|
return { transaction: tx, success: true };
|
|
} catch (error) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
error: `Failed to build transaction: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function prepareAndBuildBidTransaction(
|
|
provider: SuiClient,
|
|
address: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
bidAmountMist: bigint,
|
|
packageId: string,
|
|
moduleName: string
|
|
): Promise<{
|
|
transaction: Transaction;
|
|
success: boolean;
|
|
preparationNeeded?: boolean;
|
|
message?: string;
|
|
}> {
|
|
const result = await buildPlaceBidTxWithCoinSelection(
|
|
provider,
|
|
address,
|
|
marketplaceObjectId,
|
|
listingId,
|
|
bidAmountMist,
|
|
packageId,
|
|
moduleName
|
|
);
|
|
|
|
if (result.success) return result;
|
|
|
|
if (result.needsPreparation) {
|
|
const prep = await buildPrepareCoinsTx(provider, address);
|
|
if (prep.success) {
|
|
return {
|
|
transaction: prep.transaction,
|
|
success: false,
|
|
preparationNeeded: true,
|
|
message:
|
|
"Execute this preparation transaction first, then retry placing the bid.",
|
|
};
|
|
} else {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
message: prep.reason || "Unknown preparation failure",
|
|
};
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export async function buildPlaceStakeTxWithCoinSelection(
|
|
provider: SuiClient,
|
|
address: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
stakeAmountMist: bigint,
|
|
packageId: string,
|
|
moduleName: string
|
|
): Promise<{
|
|
transaction: Transaction;
|
|
success: boolean;
|
|
needsPreparation?: boolean;
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
console.log("Starting to build stake transaction with params:", {
|
|
address,
|
|
marketplaceObjectId,
|
|
listingId,
|
|
stakeAmountMist: stakeAmountMist.toString(),
|
|
packageId,
|
|
moduleName,
|
|
});
|
|
|
|
const { data: coinData } = await provider.getCoins({
|
|
owner: address,
|
|
coinType: "0x2::sui::SUI",
|
|
});
|
|
|
|
if (!coinData || coinData.length === 0) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
error: "No SUI coins available.",
|
|
};
|
|
}
|
|
|
|
const sortedCoins = [...coinData].sort((a, b) =>
|
|
Number(BigInt(b.balance) - BigInt(a.balance))
|
|
);
|
|
|
|
const estimatedGasFee = BigInt(30_000_000); // 0.03 SUI
|
|
const totalBalance = sortedCoins.reduce(
|
|
(sum, coin) => sum + BigInt(coin.balance),
|
|
BigInt(0)
|
|
);
|
|
|
|
if (totalBalance < stakeAmountMist + estimatedGasFee) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
error: `Insufficient balance. Need ${
|
|
Number(stakeAmountMist + estimatedGasFee) / 1_000_000_000
|
|
} SUI, but have ${Number(totalBalance) / 1_000_000_000} SUI.`,
|
|
};
|
|
}
|
|
|
|
let gasCoin = null;
|
|
let stakeCoin = null;
|
|
|
|
// Prepare gasCoin (first step)
|
|
for (const coin of sortedCoins) {
|
|
if (
|
|
BigInt(coin.balance) >= estimatedGasFee &&
|
|
BigInt(coin.balance) < stakeAmountMist + estimatedGasFee
|
|
) {
|
|
gasCoin = coin;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!gasCoin) {
|
|
for (const coin of sortedCoins) {
|
|
if (BigInt(coin.balance) >= estimatedGasFee) {
|
|
gasCoin = coin;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare stakeCoin (second step)
|
|
if (gasCoin) {
|
|
for (const coin of sortedCoins) {
|
|
if (
|
|
coin.coinObjectId !== gasCoin.coinObjectId &&
|
|
BigInt(coin.balance) >= stakeAmountMist
|
|
) {
|
|
stakeCoin = coin;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no gas or stake coin available, we need preparation
|
|
if (!gasCoin || !stakeCoin) {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
needsPreparation: true,
|
|
error:
|
|
"Need to split coins first. Cannot use the same coin for gas and staking.",
|
|
};
|
|
}
|
|
|
|
const tx = new Transaction();
|
|
tx.setSender(address);
|
|
tx.setGasBudget(Number(estimatedGasFee));
|
|
tx.setGasPayment([
|
|
{
|
|
objectId: gasCoin.coinObjectId,
|
|
version: gasCoin.version,
|
|
digest: gasCoin.digest,
|
|
},
|
|
]);
|
|
|
|
const stakeCoinObj = tx.object(stakeCoin.coinObjectId);
|
|
|
|
// Handling coin split if necessary (for stake coin)
|
|
if (BigInt(stakeCoin.balance) > stakeAmountMist) {
|
|
const splitStakeCoins = tx.splitCoins(stakeCoinObj, [
|
|
tx.pure.u64(stakeAmountMist.toString()),
|
|
]);
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::stake_on_listing`,
|
|
arguments: [
|
|
tx.object(marketplaceObjectId),
|
|
tx.pure.address(listingId),
|
|
splitStakeCoins[0],
|
|
],
|
|
});
|
|
} else {
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::stake_on_listing`,
|
|
arguments: [
|
|
tx.object(marketplaceObjectId),
|
|
tx.pure.address(listingId),
|
|
stakeCoinObj,
|
|
],
|
|
});
|
|
}
|
|
|
|
return { transaction: tx, success: true };
|
|
} catch (error) {
|
|
console.error("Error building stake transaction:", error);
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
error: `Failed to build transaction: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function prepareAndBuildStakeTransaction(
|
|
provider: SuiClient,
|
|
address: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
stakeAmountMist: bigint,
|
|
packageId: string,
|
|
moduleName: string
|
|
): Promise<{
|
|
transaction: Transaction;
|
|
success: boolean;
|
|
preparationNeeded?: boolean;
|
|
message?: string;
|
|
}> {
|
|
const result = await buildPlaceStakeTxWithCoinSelection(
|
|
provider,
|
|
address,
|
|
marketplaceObjectId,
|
|
listingId,
|
|
stakeAmountMist,
|
|
packageId,
|
|
moduleName
|
|
);
|
|
|
|
if (result.success) return result;
|
|
|
|
if (result.needsPreparation) {
|
|
const prep = await buildPrepareCoinsTx(provider, address);
|
|
if (prep.success) {
|
|
return {
|
|
transaction: prep.transaction,
|
|
success: false,
|
|
preparationNeeded: true,
|
|
message:
|
|
"Execute this preparation transaction first, then retry placing the stake.",
|
|
};
|
|
} else {
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
message: prep.reason || "Unknown preparation failure",
|
|
};
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// export async function buildPlaceStakeTxWithCoinSelection(
|
|
// provider: SuiClient,
|
|
// address: string,
|
|
// marketplaceObjectId: string,
|
|
// listingId: string,
|
|
// stakeAmountMist: bigint,
|
|
// packageId: string,
|
|
// moduleName: string
|
|
// ): Promise<{ transaction: Transaction; success: boolean; error?: string }> {
|
|
// try {
|
|
// console.log("Starting to build stake transaction with params:", {
|
|
// address,
|
|
// marketplaceObjectId,
|
|
// listingId,
|
|
// stakeAmountMist: stakeAmountMist.toString(),
|
|
// packageId,
|
|
// moduleName,
|
|
// });
|
|
|
|
// const { data: coinData } = await provider.getCoins({
|
|
// owner: address,
|
|
// coinType: "0x2::sui::SUI",
|
|
// });
|
|
|
|
// if (!coinData || coinData.length === 0) {
|
|
// return {
|
|
// transaction: new Transaction(),
|
|
// success: false,
|
|
// error: "No SUI coins available.",
|
|
// };
|
|
// }
|
|
|
|
// const sortedCoins = [...coinData].sort((a, b) =>
|
|
// Number(BigInt(b.balance) - BigInt(a.balance))
|
|
// );
|
|
|
|
// const estimatedGasFee = BigInt(30_000_000); // 0.03 SUI
|
|
// const totalBalance = sortedCoins.reduce(
|
|
// (sum, coin) => sum + BigInt(coin.balance),
|
|
// BigInt(0)
|
|
// );
|
|
|
|
// if (totalBalance < stakeAmountMist + estimatedGasFee) {
|
|
// return {
|
|
// transaction: new Transaction(),
|
|
// success: false,
|
|
// error: `Insufficient balance. Need ${
|
|
// Number(stakeAmountMist + estimatedGasFee) / 1_000_000_000
|
|
// } SUI, but have ${Number(totalBalance) / 1_000_000_000} SUI.`,
|
|
// };
|
|
// }
|
|
|
|
// let gasCoin = null;
|
|
// let stakeCoin = null;
|
|
|
|
// for (const coin of sortedCoins) {
|
|
// if (
|
|
// BigInt(coin.balance) >= estimatedGasFee &&
|
|
// BigInt(coin.balance) < stakeAmountMist + estimatedGasFee
|
|
// ) {
|
|
// gasCoin = coin;
|
|
// break;
|
|
// }
|
|
// }
|
|
|
|
// if (!gasCoin) {
|
|
// for (const coin of sortedCoins) {
|
|
// if (BigInt(coin.balance) >= estimatedGasFee) {
|
|
// gasCoin = coin;
|
|
// break;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// if (gasCoin) {
|
|
// for (const coin of sortedCoins) {
|
|
// if (
|
|
// coin.coinObjectId !== gasCoin.coinObjectId &&
|
|
// BigInt(coin.balance) >= stakeAmountMist
|
|
// ) {
|
|
// stakeCoin = coin;
|
|
// break;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// if (!gasCoin || !stakeCoin) {
|
|
// return {
|
|
// transaction: new Transaction(),
|
|
// success: false,
|
|
// error:
|
|
// "Need to split coins first. Cannot use the same coin for gas and staking.",
|
|
// };
|
|
// }
|
|
|
|
// const tx = new Transaction();
|
|
// tx.setSender(address);
|
|
// tx.setGasBudget(Number(estimatedGasFee));
|
|
// tx.setGasPayment([
|
|
// {
|
|
// objectId: gasCoin.coinObjectId,
|
|
// version: gasCoin.version,
|
|
// digest: gasCoin.digest,
|
|
// },
|
|
// ]);
|
|
|
|
// const stakeCoinObj = tx.object(stakeCoin.coinObjectId);
|
|
|
|
// if (BigInt(stakeCoin.balance) > stakeAmountMist) {
|
|
// const splitStakeCoins = tx.splitCoins(stakeCoinObj, [
|
|
// tx.pure.u64(stakeAmountMist.toString()),
|
|
// ]);
|
|
|
|
// tx.moveCall({
|
|
// target: `${packageId}::${moduleName}::stake_on_listing`,
|
|
// arguments: [
|
|
// tx.object(marketplaceObjectId),
|
|
// tx.pure.address(listingId),
|
|
// splitStakeCoins[0],
|
|
// ],
|
|
// });
|
|
// } else {
|
|
// tx.moveCall({
|
|
// target: `${packageId}::${moduleName}::stake_on_listing`,
|
|
// arguments: [
|
|
// tx.object(marketplaceObjectId),
|
|
// tx.pure.address(listingId),
|
|
// stakeCoinObj,
|
|
// ],
|
|
// });
|
|
// }
|
|
|
|
// return { transaction: tx, success: true };
|
|
// } catch (error) {
|
|
// console.error("Error building stake transaction:", error);
|
|
// return {
|
|
// transaction: new Transaction(),
|
|
// success: false,
|
|
// error: `Failed to build transaction: ${
|
|
// error instanceof Error ? error.message : String(error)
|
|
// }`,
|
|
// };
|
|
// }
|
|
// }
|
|
|
|
export async function listNft(
|
|
softListingId: string,
|
|
listPrice: number,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
nftId: string
|
|
): Promise<{ transaction: Transaction; success: boolean; error?: string }> {
|
|
try {
|
|
const tx = new Transaction();
|
|
|
|
const estimatedGasFee = BigInt(50000000); // 0.05 SUI
|
|
tx.setGasBudget(Number(estimatedGasFee));
|
|
|
|
const nftTypeArg =
|
|
"0x11fe6fadbdcf82659757c793e7337f8af5198a9f35cbad68a2337d01395eb657::sigillum_nft::PhotoNFT";
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::convert_to_real_listing`,
|
|
typeArguments: [nftTypeArg],
|
|
arguments: [
|
|
tx.object(marketplaceObjectId),
|
|
tx.pure.address(softListingId),
|
|
tx.pure.u64(listPrice.toString()),
|
|
tx.object(nftId),
|
|
],
|
|
});
|
|
|
|
return { transaction: tx, success: true };
|
|
} catch (error) {
|
|
console.error("Error building convert listing tx:", error);
|
|
return {
|
|
transaction: new Transaction(),
|
|
success: false,
|
|
error: "Failed to build convert_to_real_listing transaction",
|
|
};
|
|
}
|
|
}
|
|
|
|
export const buildRelistNftTx = async (
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
nftId: string,
|
|
newPrice: number,
|
|
newMinBid: number,
|
|
newEndTime: number,
|
|
packageId: string,
|
|
moduleName: string
|
|
): Promise<Transaction> => {
|
|
try {
|
|
const tx = new Transaction();
|
|
const estimatedGasFee = BigInt(50000000); // 0.05 SUI
|
|
tx.setGasBudget(Number(estimatedGasFee));
|
|
|
|
// Use the same NFT type as in your existing code
|
|
// This should match the type of your NFT - update if necessary
|
|
const nftTypeArg =
|
|
"0x11fe6fadbdcf82659757c793e7337f8af5198a9f35cbad68a2337d01395eb657::sigillum_nft::PhotoNFT";
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::relist_on_same_listing`,
|
|
typeArguments: [nftTypeArg],
|
|
arguments: [
|
|
tx.object(marketplaceObjectId),
|
|
tx.pure.address(listingId),
|
|
tx.object(nftId),
|
|
tx.pure.u64(newPrice.toString()),
|
|
tx.pure.u64(newMinBid.toString()),
|
|
tx.pure.u64(newEndTime.toString()),
|
|
],
|
|
});
|
|
|
|
return tx;
|
|
} catch (error) {
|
|
console.error("Error building relist transaction:", error);
|
|
throw new Error("Failed to build relist_on_same_listing transaction");
|
|
}
|
|
};
|
|
|
|
export async function getUserStake(
|
|
provider: SuiClient,
|
|
packageId: string,
|
|
moduleName: string,
|
|
marketplaceObjectId: string,
|
|
listingId: string,
|
|
stakerAddress: string,
|
|
callerAddress: string | null = null
|
|
) {
|
|
if (!callerAddress) return { hasStaked: false, stakeAmount: 0 };
|
|
const tx = new Transaction();
|
|
|
|
tx.moveCall({
|
|
target: `${packageId}::${moduleName}::get_user_stake`,
|
|
arguments: [
|
|
tx.object(marketplaceObjectId),
|
|
tx.pure.address(listingId),
|
|
tx.pure.address(stakerAddress),
|
|
],
|
|
});
|
|
|
|
try {
|
|
const result = await provider.devInspectTransactionBlock({
|
|
sender: callerAddress,
|
|
transactionBlock: tx,
|
|
});
|
|
|
|
// For debugging (can be removed in production)
|
|
// console.log("Raw Result:", JSON.stringify(result, null, 2));
|
|
|
|
const returnValues = result?.results?.[0]?.returnValues;
|
|
if (!returnValues || !Array.isArray(returnValues)) {
|
|
return { hasStaked: false, stakeAmount: 0 };
|
|
}
|
|
|
|
// Let's find the boolean (hasStaked) and the u64 (stakeAmount) in the results
|
|
let hasStaked = false;
|
|
let stakeAmount = 0;
|
|
|
|
// Find the boolean value first
|
|
const boolValues = returnValues.filter(
|
|
([value, type]) => type === "bool" && Array.isArray(value)
|
|
);
|
|
|
|
if (boolValues.length > 0) {
|
|
hasStaked = boolValues[0][0][0] === 1;
|
|
}
|
|
|
|
// Find the stake amount - we need to be smart about this
|
|
// There might be multiple u64 values, but we want the one that represents the stake
|
|
const u64Values = returnValues.filter(
|
|
([value, type]) =>
|
|
type === "u64" && Array.isArray(value) && value.length === 8
|
|
);
|
|
|
|
if (u64Values.length > 0) {
|
|
// If hasStaked is false, the stake amount should be 0
|
|
// If hasStaked is true, the stake amount should be > 0
|
|
for (const [valueArray] of u64Values) {
|
|
const amount = parseU64FromByteArray(valueArray);
|
|
|
|
// If we find a value that matches our expectation based on hasStaked, use it
|
|
if ((!hasStaked && amount === 0) || (hasStaked && amount > 0)) {
|
|
stakeAmount = amount;
|
|
break;
|
|
}
|
|
|
|
// Otherwise, just take the first u64 as a fallback
|
|
if (stakeAmount === 0) {
|
|
stakeAmount = amount;
|
|
}
|
|
}
|
|
}
|
|
|
|
return { hasStaked, stakeAmount };
|
|
} catch (error) {
|
|
console.error("Error getting user stake:", error);
|
|
return { hasStaked: false, stakeAmount: 0 };
|
|
}
|
|
}
|