diff --git a/src/api/types/LaunchpadDtos.ts b/src/api/types/LaunchpadDtos.ts index 0d591ec..5ce1530 100644 --- a/src/api/types/LaunchpadDtos.ts +++ b/src/api/types/LaunchpadDtos.ts @@ -39,6 +39,7 @@ import { import { BigNumberIsNotNegative, BigNumberLessThanOrEqualOther, BigNumberMax } from "../validators"; import { IsNonZeroBigNumber } from "../validators"; +import { LaunchpadTradeData } from "./LaunchpadTradeData"; export class ReverseBondingCurveConfigurationChainObject extends ChainObject { @BigNumberProperty() @@ -322,6 +323,11 @@ export class TradeResDto { @IsString() public totalTokenSold: string; + + @IsOptional() + @ValidateNested() + @Type(() => LaunchpadTradeData) + public tradeData?: LaunchpadTradeData; } export class FetchSaleDto extends ChainCallDTO { diff --git a/src/api/types/LaunchpadTradeData.ts b/src/api/types/LaunchpadTradeData.ts new file mode 100644 index 0000000..4a8f0a9 --- /dev/null +++ b/src/api/types/LaunchpadTradeData.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + BigNumberIsNotNegative, + BigNumberProperty, + ChainKey, + ChainObject, + IsUserAlias, + UserAlias +} from "@gala-chain/api"; +import BigNumber from "bignumber.js"; +import { Exclude } from "class-transformer"; +import { IsInt, IsNotEmpty } from "class-validator"; +import { JSONSchema } from "class-validator-jsonschema"; + +export interface ILaunchpadTradeData { + vaultAddress: UserAlias; + galaVolumeTraded: BigNumber; + createdAt: number; + lastUpdated: number; +} + +@JSONSchema({ + description: + "LaunchpadSale trade data, metrics, and/or analytics aggregated throughout the lifetime of the sale." +}) +export class LaunchpadTradeData extends ChainObject { + @Exclude() + static INDEX_KEY = "GCLPTD"; //GalaChain LaunchPad Trade Data + + @ChainKey({ position: 0 }) + @IsUserAlias() + @IsNotEmpty() + public vaultAddress: UserAlias; + + @BigNumberIsNotNegative() + @BigNumberProperty() + public galaVolumeTraded: BigNumber; + + @IsInt() + public createdAt: number; + + @IsInt() + public lastUpdated: number; + + // constructor supports both new LaunchpadTradeData() and plainToInstance() / createValidChainObject() + // instance initialization styles + constructor(data: ILaunchpadTradeData) { + super(); + this.vaultAddress = data?.vaultAddress ?? ""; + this.galaVolumeTraded = data?.galaVolumeTraded ?? new BigNumber(0); + this.createdAt = data?.createdAt ?? 0; + this.lastUpdated = data?.lastUpdated ?? 0; + } +} diff --git a/src/api/types/index.ts b/src/api/types/index.ts index 5f5e5c1..dcc1270 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -17,4 +17,5 @@ export * from "./LaunchpadDtos"; export * from "./LaunchpadFinalizeAllocation"; export * from "./LaunchpadFeeConfig"; export * from "./LaunchpadSale"; +export * from "./LaunchpadTradeData"; export * from "./LaunchpadBatchSubmitAuthorities"; diff --git a/src/chaincode/launchpad/buyExactToken.ts b/src/chaincode/launchpad/buyExactToken.ts index 87d5b2a..177100e 100644 --- a/src/chaincode/launchpad/buyExactToken.ts +++ b/src/chaincode/launchpad/buyExactToken.ts @@ -20,7 +20,9 @@ import { SlippageToleranceExceededError } from "../../api/utils/error"; import { fetchAndValidateSale } from "../utils"; import { callNativeTokenIn } from "./callNativeTokenIn"; import { transferTransactionFees } from "./fees"; +import { fetchOrCreateLaunchpadTradeData } from "./fetchLaunchpadTradeData"; import { finalizeSale } from "./finaliseSale"; +import { writeTradeData } from "./writeTradeData"; /** * Executes the purchase of an exact amount of tokens in a token sale. @@ -100,6 +102,13 @@ export async function buyExactToken( sale.buyToken(tokensToBuy, nativeTokensRequired); await putChainObject(ctx, sale); + const galaVolumeTraded = nativeTokensRequired.abs(); + + const tradeData = await writeTradeData(ctx, { + vaultAddress: sale.vaultAddress, + galaVolumeTraded: galaVolumeTraded + }); + // If the sale is finalized, create a V3 pool and add liquidity if (isSaleFinalized) { await finalizeSale(ctx, sale); diff --git a/src/chaincode/launchpad/buyWithNative.ts b/src/chaincode/launchpad/buyWithNative.ts index c674dc7..ee27795 100644 --- a/src/chaincode/launchpad/buyWithNative.ts +++ b/src/chaincode/launchpad/buyWithNative.ts @@ -21,6 +21,7 @@ import { fetchAndValidateSale } from "../utils"; import { callMemeTokenOut } from "./callMemeTokenOut"; import { transferTransactionFees } from "./fees"; import { finalizeSale } from "./finaliseSale"; +import { writeTradeData } from "./writeTradeData"; /** * Executes the purchase of tokens using a specified amount of native tokens. @@ -104,6 +105,13 @@ export async function buyWithNative( sale.buyToken(tokensToBuy, nativeTokensRequired); await putChainObject(ctx, sale); + const galaVolumeTraded = nativeTokensRequired.abs(); + + const tradeData = await writeTradeData(ctx, { + vaultAddress: sale.vaultAddress, + galaVolumeTraded: galaVolumeTraded + }); + // Finalize sale if it's complete if (isSaleFinalized) { await finalizeSale(ctx, sale); diff --git a/src/chaincode/launchpad/createSale.ts b/src/chaincode/launchpad/createSale.ts index f36bf9b..27166fb 100644 --- a/src/chaincode/launchpad/createSale.ts +++ b/src/chaincode/launchpad/createSale.ts @@ -32,6 +32,7 @@ import { } from "../../api/types"; import { PreConditionFailedError } from "../../api/utils/error"; import { buyWithNative } from "./buyWithNative"; +import { fetchOrCreateLaunchpadTradeData } from "./fetchLaunchpadTradeData"; /** * Creates a new token sale (Launchpad) in the GalaChain environment. @@ -154,6 +155,10 @@ export async function createSale( await putChainObject(ctx, launchpad); } + const tradeData = await fetchOrCreateLaunchpadTradeData(ctx, { vaultAddress: launchpad.vaultAddress }); + + await putChainObject(ctx, tradeData); + // Return the response object return { image: launchpadDetails.tokenImage, diff --git a/src/chaincode/launchpad/fetchLaunchpadTradeData.ts b/src/chaincode/launchpad/fetchLaunchpadTradeData.ts new file mode 100644 index 0000000..883362d --- /dev/null +++ b/src/chaincode/launchpad/fetchLaunchpadTradeData.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ChainError, + ErrorCode, + NotFoundError, + asValidUserAlias, + createValidChainObject +} from "@gala-chain/api"; +import { GalaChainContext, getObjectByKey } from "@gala-chain/chaincode"; +import BigNumber from "bignumber.js"; + +import { LaunchpadTradeData } from "../../api/types"; + +/** + * Fetches the trade data a specific token sale (LaunchpadSale) using the sale address. + * + * This function retrieves the trade data object from the chain using a composite key derived + * from the sale address. If the trade data record is not found, a newly initialized chain entry is returned. + * + * @param ctx - The context object providing access to the GalaChain environment. + * @param data - An object containing the sale address: + * - `vaultAddress`: The address of the sale to be fetched. + * + * @returns A promise that resolves to a `LaunchpadTradeData` object containing details about + * the specified token sale. + * + */ +export async function fetchOrCreateLaunchpadTradeData( + ctx: GalaChainContext, + data: { vaultAddress: string } +): Promise { + const { vaultAddress } = data; + + const key = ctx.stub.createCompositeKey(LaunchpadTradeData.INDEX_KEY, [vaultAddress]); + + const tradeData = await getObjectByKey(ctx, LaunchpadTradeData, key).catch((e) => { + const error = ChainError.from(e); + + if (!error.matches(ErrorCode.NOT_FOUND)) { + throw error; + } + + return createValidChainObject(LaunchpadTradeData, { + vaultAddress: asValidUserAlias(vaultAddress), + createdAt: ctx.txUnixTime, + lastUpdated: ctx.txUnixTime, + galaVolumeTraded: new BigNumber(0) + }); + }); + + return tradeData; +} diff --git a/src/chaincode/launchpad/finaliseSale.ts b/src/chaincode/launchpad/finaliseSale.ts index f9e5da2..64fd352 100644 --- a/src/chaincode/launchpad/finaliseSale.ts +++ b/src/chaincode/launchpad/finaliseSale.ts @@ -41,6 +41,7 @@ import Decimal from "decimal.js"; import { LaunchpadFinalizeFeeAllocation, LaunchpadSale } from "../../api/types"; import { PreConditionFailedError } from "../../api/utils/error"; import { fetchLaunchpadFeeAddress, getBondingConstants } from "../utils"; +import { fetchOrCreateLaunchpadTradeData } from "./fetchLaunchpadTradeData"; export async function finalizeSale(ctx: GalaChainContext, sale: LaunchpadSale): Promise { const key = ctx.stub.createCompositeKey(LaunchpadFinalizeFeeAllocation.INDEX_KEY, []); @@ -59,13 +60,26 @@ export async function finalizeSale(ctx: GalaChainContext, sale: LaunchpadSale): const memeToken = sale.fetchSellingTokenInstanceKey(); const vaultAddressAlias = await resolveUserAlias(ctx, sale.vaultAddress); + const creatorRewardsV1 = new BigNumber(sale.nativeTokenQuantity) + .times(ownerAllocationPercentage) + .decimalPlaces(LaunchpadSale.NATIVE_TOKEN_DECIMALS, BigNumber.ROUND_DOWN); + + const tradeData = await fetchOrCreateLaunchpadTradeData(ctx, { vaultAddress: sale.vaultAddress }); + + const creatorRewardsV2 = tradeData.galaVolumeTraded + .times(ownerAllocationPercentage) + .decimalPlaces(LaunchpadSale.NATIVE_TOKEN_DECIMALS, BigNumber.ROUND_DOWN); + + // todo: finalize business logic for pre-existing sales created prior to new total volume calculation + const creatorRewards = creatorRewardsV2.isGreaterThan(creatorRewardsV1) + ? creatorRewardsV2 + : creatorRewardsV1; + await transferToken(ctx, { from: vaultAddressAlias, to: sale.saleOwner, tokenInstanceKey: nativeToken, - quantity: new BigNumber(sale.nativeTokenQuantity) - .times(ownerAllocationPercentage) - .decimalPlaces(LaunchpadSale.NATIVE_TOKEN_DECIMALS, BigNumber.ROUND_DOWN), + quantity: creatorRewards, allowancesToUse: [], authorizedOnBehalf: { callingOnBehalf: vaultAddressAlias, diff --git a/src/chaincode/launchpad/index.ts b/src/chaincode/launchpad/index.ts index cb50e2f..cffc161 100644 --- a/src/chaincode/launchpad/index.ts +++ b/src/chaincode/launchpad/index.ts @@ -20,6 +20,7 @@ export * from "./callNativeTokenIn"; export * from "./callNativeTokenOut"; export * from "./createSale"; export * from "./fetchSaleDetails"; +export * from "./fetchLaunchpadTradeData"; export * from "./sellExactToken"; export * from "./finalizeTokenAllocation"; export * from "./sellWithNative"; diff --git a/src/chaincode/launchpad/sellExactToken.ts b/src/chaincode/launchpad/sellExactToken.ts index d738592..913060b 100644 --- a/src/chaincode/launchpad/sellExactToken.ts +++ b/src/chaincode/launchpad/sellExactToken.ts @@ -21,6 +21,7 @@ import { SlippageToleranceExceededError } from "../../api/utils/error"; import { fetchAndValidateSale } from "../utils"; import { callNativeTokenOut } from "./callNativeTokenOut"; import { payReverseBondingCurveFee, transferTransactionFees } from "./fees"; +import { writeTradeData } from "./writeTradeData"; /** * Executes the sale of an exact amount of tokens for native tokens (e.g., GALA). @@ -111,6 +112,13 @@ export async function sellExactToken( sale.sellToken(tokensBeingSold, nativeTokensPayout); await putChainObject(ctx, sale); + const galaVolumeTraded = nativeTokensPayout.abs(); + + const tradeData = await writeTradeData(ctx, { + vaultAddress: sale.vaultAddress, + galaVolumeTraded: galaVolumeTraded + }); + const token = await fetchTokenClass(ctx, sale.sellingToken); return { inputQuantity: tokensBeingSold.toFixed(), diff --git a/src/chaincode/launchpad/sellWithNative.ts b/src/chaincode/launchpad/sellWithNative.ts index bf3f23f..aafc6d1 100644 --- a/src/chaincode/launchpad/sellWithNative.ts +++ b/src/chaincode/launchpad/sellWithNative.ts @@ -27,6 +27,7 @@ import { SlippageToleranceExceededError } from "../../api/utils/error"; import { fetchAndValidateSale } from "../utils"; import { callMemeTokenIn } from "./callMemeTokenIn"; import { payReverseBondingCurveFee, transferTransactionFees } from "./fees"; +import { writeTradeData } from "./writeTradeData"; /** * Executes a sale of tokens using native tokens (e.g., GALA) in exchange for the specified token amount. @@ -127,6 +128,13 @@ export async function sellWithNative( sale.sellToken(tokensToSell, nativeTokensPayout); await putChainObject(ctx, sale); + const galaVolumeTraded = nativeTokensPayout.abs(); + + const tradeData = await writeTradeData(ctx, { + vaultAddress: sale.vaultAddress, + galaVolumeTraded: galaVolumeTraded + }); + const token = await fetchTokenClass(ctx, sale.sellingToken); return { inputQuantity: tokensToSell.toFixed(), diff --git a/src/chaincode/launchpad/writeTradeData.ts b/src/chaincode/launchpad/writeTradeData.ts new file mode 100644 index 0000000..348466d --- /dev/null +++ b/src/chaincode/launchpad/writeTradeData.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { GalaChainContext, putChainObject } from "@gala-chain/chaincode"; +import BigNumber from "bignumber.js"; + +import { LaunchpadTradeData } from "../../api/types"; +import { fetchOrCreateLaunchpadTradeData } from "./fetchLaunchpadTradeData"; + +export interface IWriteTradeData { + vaultAddress: string; + galaVolumeTraded: BigNumber; +} + +export async function writeTradeData( + ctx: GalaChainContext, + data: IWriteTradeData +): Promise { + const { vaultAddress, galaVolumeTraded } = data; + + const tradeData = await fetchOrCreateLaunchpadTradeData(ctx, { vaultAddress }); + + tradeData.galaVolumeTraded = tradeData.galaVolumeTraded.plus(galaVolumeTraded); + tradeData.lastUpdated = ctx.txUnixTime; + + await putChainObject(ctx, tradeData); + + return tradeData; +}