@gala-chain/launchpad 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/src/api/types/LaunchpadDtos.d.ts +3 -1
  3. package/lib/src/api/types/LaunchpadDtos.d.ts.map +1 -1
  4. package/lib/src/api/types/LaunchpadDtos.js +19 -1
  5. package/lib/src/api/types/LaunchpadDtos.js.map +1 -1
  6. package/lib/src/api/types/LaunchpadSale.d.ts +8 -1
  7. package/lib/src/api/types/LaunchpadSale.d.ts.map +1 -1
  8. package/lib/src/api/types/LaunchpadSale.js +37 -8
  9. package/lib/src/api/types/LaunchpadSale.js.map +1 -1
  10. package/lib/src/chaincode/launchpad/buyWithNative.d.ts.map +1 -1
  11. package/lib/src/chaincode/launchpad/buyWithNative.js +4 -1
  12. package/lib/src/chaincode/launchpad/buyWithNative.js.map +1 -1
  13. package/lib/src/chaincode/launchpad/callMemeTokenIn.d.ts.map +1 -1
  14. package/lib/src/chaincode/launchpad/callMemeTokenIn.js +6 -4
  15. package/lib/src/chaincode/launchpad/callMemeTokenIn.js.map +1 -1
  16. package/lib/src/chaincode/launchpad/callMemeTokenOut.d.ts.map +1 -1
  17. package/lib/src/chaincode/launchpad/callMemeTokenOut.js +19 -8
  18. package/lib/src/chaincode/launchpad/callMemeTokenOut.js.map +1 -1
  19. package/lib/src/chaincode/launchpad/callNativeTokenIn.d.ts.map +1 -1
  20. package/lib/src/chaincode/launchpad/callNativeTokenIn.js +13 -6
  21. package/lib/src/chaincode/launchpad/callNativeTokenIn.js.map +1 -1
  22. package/lib/src/chaincode/launchpad/callNativeTokenOut.d.ts.map +1 -1
  23. package/lib/src/chaincode/launchpad/callNativeTokenOut.js +6 -4
  24. package/lib/src/chaincode/launchpad/callNativeTokenOut.js.map +1 -1
  25. package/lib/src/chaincode/launchpad/createSale.d.ts.map +1 -1
  26. package/lib/src/chaincode/launchpad/createSale.js +22 -6
  27. package/lib/src/chaincode/launchpad/createSale.js.map +1 -1
  28. package/lib/src/chaincode/launchpad/finaliseSale.js +2 -2
  29. package/lib/src/chaincode/launchpad/finaliseSale.js.map +1 -1
  30. package/lib/src/chaincode/launchpad/sellExactToken.js +1 -1
  31. package/lib/src/chaincode/launchpad/sellExactToken.js.map +1 -1
  32. package/lib/src/chaincode/utils/launchpadSaleUtils.d.ts +1 -1
  33. package/lib/src/chaincode/utils/launchpadSaleUtils.d.ts.map +1 -1
  34. package/lib/src/chaincode/utils/launchpadSaleUtils.js +14 -2
  35. package/lib/src/chaincode/utils/launchpadSaleUtils.js.map +1 -1
  36. package/lib/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +1 -1
  38. package/src/api/types/LaunchpadDtos.ts +22 -1
  39. package/src/api/types/LaunchpadSale.ts +51 -8
  40. package/src/chaincode/launchpad/buyExactToken.spec.ts +234 -1
  41. package/src/chaincode/launchpad/buyWithNative.spec.ts +163 -8
  42. package/src/chaincode/launchpad/buyWithNative.ts +5 -1
  43. package/src/chaincode/launchpad/callMemeTokenIn.ts +11 -4
  44. package/src/chaincode/launchpad/callMemeTokenOut.spec.ts +1 -1
  45. package/src/chaincode/launchpad/callMemeTokenOut.ts +24 -8
  46. package/src/chaincode/launchpad/callNativeTokenIn.ts +18 -7
  47. package/src/chaincode/launchpad/callNativeTokenOut.ts +10 -4
  48. package/src/chaincode/launchpad/createSale.ts +35 -7
  49. package/src/chaincode/launchpad/finaliseSale.ts +2 -2
  50. package/src/chaincode/launchpad/sellExactToken.spec.ts +139 -2
  51. package/src/chaincode/launchpad/sellExactToken.ts +1 -1
  52. package/src/chaincode/launchpad/sellWithNative.spec.ts +157 -1
  53. package/src/chaincode/utils/launchpadSaleUtils.ts +18 -2
@@ -29,15 +29,26 @@ function calculateTokensPurchasable(
29
29
  nativeTokens: Decimal,
30
30
  totalTokensSold: Decimal,
31
31
  nativeTokenDecimals: number,
32
- sellingTokenDecimals: number
32
+ sellingTokenDecimals: number,
33
+ adjustableSupplyMultiplier?: number
33
34
  ): [string, string] {
34
- const basePrice = new Decimal(LaunchpadSale.BASE_PRICE);
35
- const { exponentFactor, euler, decimals } = getBondingConstants();
35
+ const basePrice =
36
+ adjustableSupplyMultiplier && adjustableSupplyMultiplier > 0
37
+ ? new Decimal(LaunchpadSale.BASE_PRICE).dividedBy(adjustableSupplyMultiplier)
38
+ : new Decimal(LaunchpadSale.BASE_PRICE);
39
+
40
+ const { exponentFactor, euler, decimals } = getBondingConstants(adjustableSupplyMultiplier);
36
41
 
37
42
  // Round native tokens, then calculate tokens based on that rounded amount
38
43
  const roundedNativeTokens = nativeTokens.toDecimalPlaces(nativeTokenDecimals, Decimal.ROUND_UP);
39
44
 
40
- // Calculate tokens purchasable: newTokens = (decimals / exponentFactor) * ln((nativeTokens * exponentFactor / basePrice) + e^(exponentFactor * totalTokensSold / decimals)) - totalTokensSold
45
+ // Calculate tokens purchasable:
46
+ // newTokens = (decimals / exponentFactor) *
47
+ // ln(
48
+ // (nativeTokens * exponentFactor / basePrice) +
49
+ // e^(exponentFactor * totalTokensSold / decimals)
50
+ // ) -
51
+ // totalTokensSold
41
52
  // Where:
42
53
  // constant = nativeTokens * exponentFactor / basePrice
43
54
  // exponent1 = exponentFactor * totalTokensSold / decimals
@@ -55,9 +66,13 @@ function calculateTokensPurchasable(
55
66
  const result = lnEthScaledBase.minus(totalTokensSold);
56
67
  let roundedResult = result.toDecimalPlaces(sellingTokenDecimals, Decimal.ROUND_DOWN);
57
68
 
58
- // Cap total supply to 10 million
59
- if (roundedResult.add(totalTokensSold).greaterThan(new Decimal("1e+7"))) {
60
- roundedResult = new Decimal("1e+7").minus(new Decimal(totalTokensSold));
69
+ // Cap total supply
70
+ const supplyCap = adjustableSupplyMultiplier
71
+ ? new BigNumber(1e7).times(adjustableSupplyMultiplier).toString()
72
+ : "1e+7";
73
+
74
+ if (roundedResult.add(totalTokensSold).greaterThan(new Decimal(supplyCap))) {
75
+ roundedResult = new Decimal(supplyCap).minus(new Decimal(totalTokensSold));
61
76
  }
62
77
 
63
78
  return [roundedNativeTokens.toFixed(), roundedResult.toFixed()];
@@ -119,7 +134,8 @@ export async function callMemeTokenOut(
119
134
  nativeTokens,
120
135
  totalTokensSold,
121
136
  nativeTokenDecimals,
122
- sellingTokenDecimals
137
+ sellingTokenDecimals,
138
+ sale?.adjustableSupplyMultiplier
123
139
  );
124
140
 
125
141
  // Fetch fee configuration and return result
@@ -29,15 +29,25 @@ function calculateNativeTokensRequired(
29
29
  tokensToBuy: Decimal,
30
30
  totalTokensSold: Decimal,
31
31
  sellingTokenDecimals: number,
32
- nativeTokenDecimals: number
32
+ nativeTokenDecimals: number,
33
+ adjustableSupplyMultiplier?: number
33
34
  ): [string, string] {
34
- const basePrice = new Decimal(LaunchpadSale.BASE_PRICE);
35
- const { exponentFactor, euler, decimals } = getBondingConstants();
35
+ const basePrice =
36
+ adjustableSupplyMultiplier && adjustableSupplyMultiplier > 0
37
+ ? new Decimal(LaunchpadSale.BASE_PRICE).dividedBy(adjustableSupplyMultiplier)
38
+ : new Decimal(LaunchpadSale.BASE_PRICE);
39
+
40
+ const { exponentFactor, euler, decimals } = getBondingConstants(adjustableSupplyMultiplier);
36
41
 
37
42
  // Round tokens first, then calculate native tokens based on that rounded amount
38
43
  const roundedTokensToBuy = tokensToBuy.toDecimalPlaces(sellingTokenDecimals, Decimal.ROUND_DOWN);
39
44
 
40
- // Calculate native tokens required: price = (basePrice / exponentFactor) * (e^(exponentFactor * (totalTokensSold + tokensToBuy) / decimals) - e^(exponentFactor * totalTokensSold / decimals))
45
+ // Calculate native tokens required:
46
+ // price = (basePrice / exponentFactor) *
47
+ // (
48
+ // e^(exponentFactor * (totalTokensSold + tokensToBuy) / decimals) -
49
+ // e^(exponentFactor * totalTokensSold / decimals)
50
+ // )
41
51
  // Where:
42
52
  // exponent1 = exponentFactor * (totalTokensSold + tokensToBuy) / decimals
43
53
  // exponent2 = exponentFactor * totalTokensSold / decimals
@@ -81,13 +91,13 @@ export async function callNativeTokenIn(
81
91
  ctx: GalaChainContext,
82
92
  buyTokenDTO: ExactTokenQuantityDto
83
93
  ): Promise<TradeCalculationResDto> {
84
- const sale = await fetchAndValidateSale(ctx, buyTokenDTO.vaultAddress);
94
+ const sale: LaunchpadSale = await fetchAndValidateSale(ctx, buyTokenDTO.vaultAddress);
85
95
  const totalTokensSold = new Decimal(sale.fetchTokensSold());
86
96
 
87
97
  let tokensToBuy = new Decimal(buyTokenDTO.tokenQuantity.toString());
88
98
 
89
99
  // Adjust tokensToBuy if user is trying to buy more tokens than the total supply
90
- if (tokensToBuy.add(totalTokensSold).greaterThan(new Decimal("1e+7"))) {
100
+ if (tokensToBuy.add(totalTokensSold).greaterThan(new Decimal(sale.maxSupply.toString()))) {
91
101
  tokensToBuy = new Decimal(sale.sellingTokenQuantity);
92
102
  }
93
103
 
@@ -99,7 +109,8 @@ export async function callNativeTokenIn(
99
109
  tokensToBuy,
100
110
  totalTokensSold,
101
111
  sellingTokenDecimals,
102
- nativeTokenDecimals
112
+ nativeTokenDecimals,
113
+ sale.adjustableSupplyMultiplier
103
114
  );
104
115
 
105
116
  const launchpadFeeAddressConfiguration = await fetchLaunchpadFeeAddress(ctx);
@@ -29,13 +29,18 @@ function calculateNativeTokensReceived(
29
29
  sale: LaunchpadSale,
30
30
  tokensToSellBn: BigNumber,
31
31
  sellingTokenDecimals: number,
32
- nativeTokenDecimals: number
32
+ nativeTokenDecimals: number,
33
+ adjustableSupplyMultiplier?: number
33
34
  ): [string, string] {
34
35
  const totalTokensSold = new Decimal(sale.fetchTokensSold());
35
36
 
36
37
  let tokensToSell = new Decimal(tokensToSellBn.toString());
37
- const basePrice = new Decimal(LaunchpadSale.BASE_PRICE);
38
- const { exponentFactor, euler, decimals } = getBondingConstants();
38
+ const basePrice =
39
+ adjustableSupplyMultiplier && adjustableSupplyMultiplier > 0
40
+ ? new Decimal(LaunchpadSale.BASE_PRICE).dividedBy(adjustableSupplyMultiplier)
41
+ : new Decimal(LaunchpadSale.BASE_PRICE);
42
+
43
+ const { exponentFactor, euler, decimals } = getBondingConstants(adjustableSupplyMultiplier);
39
44
 
40
45
  let newTotalTokensSold = totalTokensSold.minus(tokensToSell);
41
46
 
@@ -88,7 +93,8 @@ export async function callNativeTokenOut(
88
93
  sale,
89
94
  sellTokenDTO.tokenQuantity,
90
95
  sellingTokenDecimals,
91
- nativeTokenDecimals
96
+ nativeTokenDecimals,
97
+ sale.adjustableSupplyMultiplier
92
98
  );
93
99
  const launchpadFeeAddressConfiguration = await fetchLaunchpadFeeAddress(ctx);
94
100
 
@@ -12,7 +12,7 @@
12
12
  * See the License for the specific language governing permissions and
13
13
  * limitations under the License.
14
14
  */
15
- import { ConflictError, TokenInstanceKey, asValidUserAlias } from "@gala-chain/api";
15
+ import { ConflictError, DefaultError, TokenInstanceKey, asValidUserAlias } from "@gala-chain/api";
16
16
  import {
17
17
  GalaChainContext,
18
18
  createTokenClass,
@@ -23,7 +23,13 @@ import {
23
23
  } from "@gala-chain/chaincode";
24
24
  import BigNumber from "bignumber.js";
25
25
 
26
- import { CreateSaleResDto, CreateTokenSaleDTO, LaunchpadSale, NativeTokenQuantityDto } from "../../api/types";
26
+ import {
27
+ CreateSaleResDto,
28
+ CreateTokenSaleDTO,
29
+ LaunchpadSale,
30
+ NativeTokenQuantityDto,
31
+ SaleStatus
32
+ } from "../../api/types";
27
33
  import { PreConditionFailedError } from "../../api/utils/error";
28
34
  import { buyWithNative } from "./buyWithNative";
29
35
 
@@ -80,6 +86,8 @@ export async function createSale(
80
86
  throw new ConflictError("This token and a sale associated with it already exists");
81
87
  }
82
88
 
89
+ const supplyCapMultiplier = launchpadDetails.adjustableSupplyMultiplier ?? 1;
90
+
83
91
  // Call createTokenClass
84
92
  await createTokenClass(ctx, {
85
93
  network: "GC",
@@ -90,8 +98,8 @@ export async function createSale(
90
98
  symbol: launchpadDetails.tokenSymbol,
91
99
  description: launchpadDetails.tokenDescription,
92
100
  image: launchpadDetails.tokenImage,
93
- maxSupply: new BigNumber("2e+7"),
94
- maxCapacity: new BigNumber("2e+7"),
101
+ maxSupply: new BigNumber("2e+7").times(supplyCapMultiplier),
102
+ maxCapacity: new BigNumber("2e+7").times(supplyCapMultiplier),
95
103
  totalMintAllowance: new BigNumber(0),
96
104
  totalSupply: new BigNumber(0),
97
105
  totalBurned: new BigNumber(0),
@@ -103,12 +111,13 @@ export async function createSale(
103
111
  tokenClassKey: tokenInstanceKey.getTokenClassKey(),
104
112
  tokenInstance: new BigNumber(0),
105
113
  owner: vaultAddress,
106
- quantity: new BigNumber("2e+7")
114
+ quantity: new BigNumber("2e+7").times(supplyCapMultiplier)
107
115
  });
108
116
 
109
- //Update token class to remove the calling user as an authority in the token class
117
+ // Update token class to remove the calling user as an authority in the token class
110
118
  await updateTokenClass(ctx, {
111
119
  tokenClass: tokenInstanceKey.getTokenClassKey(),
120
+ overwriteAuthorities: true,
112
121
  authorities: [vaultAddress]
113
122
  });
114
123
 
@@ -117,7 +126,9 @@ export async function createSale(
117
126
  vaultAddress,
118
127
  tokenInstanceKey,
119
128
  launchpadDetails.reverseBondingCurveConfiguration?.toChainObject(),
120
- ctx.callingUser
129
+ ctx.callingUser,
130
+ undefined,
131
+ launchpadDetails.adjustableSupplyMultiplier
121
132
  );
122
133
 
123
134
  await putChainObject(ctx, launchpad);
@@ -131,6 +142,23 @@ export async function createSale(
131
142
  isSaleFinalized = tradeStatus.isFinalized;
132
143
  }
133
144
 
145
+ // handling the optional saleStartTime after the preBuyQuantity allows
146
+ // creators to still optionally specify a pre-buy even when a
147
+ // sale is marked Upcoming / Coming Soon.
148
+ // Otherwise `buyWithNative` would throw an error when validating the sale is active.
149
+ if (launchpadDetails.saleStartTime !== undefined) {
150
+ launchpad.saleStartTime = launchpadDetails.saleStartTime;
151
+
152
+ // handle edge case
153
+ // if a sale was immediately sold out with a pre-buy on creation,
154
+ // do not set the ended sale back to UPCOMING
155
+ if (launchpad.saleStatus !== SaleStatus.END) {
156
+ launchpad.saleStatus = SaleStatus.UPCOMING;
157
+ }
158
+
159
+ await putChainObject(ctx, launchpad);
160
+ }
161
+
134
162
  // Return the response object
135
163
  return {
136
164
  image: launchpadDetails.tokenImage,
@@ -203,8 +203,8 @@ function calculateFinalLaunchpadPrice(
203
203
  areTokensSorted: boolean
204
204
  ): { sqrtPrice: BigNumber; finalPrice: BigNumber } {
205
205
  const totalTokensSold = new Decimal(sale.fetchTokensSold());
206
- const basePrice = new Decimal(LaunchpadSale.BASE_PRICE);
207
- const { exponentFactor, euler, decimals } = getBondingConstants();
206
+ const basePrice = new Decimal(sale.basePrice.toString());
207
+ const { exponentFactor, euler, decimals } = getBondingConstants(sale.adjustableSupplyMultiplier);
208
208
 
209
209
  const exponent = exponentFactor.mul(totalTokensSold).div(decimals);
210
210
  const multiplicand = euler.pow(exponent);
@@ -21,11 +21,11 @@ import {
21
21
  asValidUserAlias,
22
22
  randomUniqueKey
23
23
  } from "@gala-chain/api";
24
- import { currency, fixture, users } from "@gala-chain/test";
24
+ import { currency, fixture, transactionSuccess, users } from "@gala-chain/test";
25
25
  import BigNumber from "bignumber.js";
26
26
  import { plainToInstance } from "class-transformer";
27
27
 
28
- import { ExactTokenQuantityDto, LaunchpadSale } from "../../api/types";
28
+ import { ExactTokenQuantityDto, LaunchpadSale, TradeResDto } from "../../api/types";
29
29
  import { LaunchpadContract } from "../LaunchpadContract";
30
30
  import launchpadgala from "../test/launchpadgala";
31
31
 
@@ -204,4 +204,141 @@ describe("sellExactToken", () => {
204
204
  expect(response.Data?.inputQuantity).toBe("500");
205
205
  expect(new BigNumber(response.Data?.outputQuantity || "0").isPositive()).toBe(true);
206
206
  });
207
+
208
+ let galaPurchaseQtyDefaultSupply: BigNumber;
209
+
210
+ test("Adjustable supply: Single transaction yields expected value for default 10 Million supply", async () => {
211
+ // Given
212
+ const multiplier = undefined;
213
+
214
+ sale = new LaunchpadSale(
215
+ vaultAddress,
216
+ currencyInstance.instanceKeyObj(),
217
+ undefined,
218
+ users.testUser1.identityKey,
219
+ undefined,
220
+ multiplier
221
+ );
222
+
223
+ const { ctx, contract } = fixture(LaunchpadContract)
224
+ .registeredUsers(users.testUser1)
225
+ .savedState(
226
+ currencyClass,
227
+ currencyInstance,
228
+ launchpadGalaClass,
229
+ launchpadGalaInstance,
230
+ sale,
231
+ salelaunchpadGalaBalance,
232
+ saleCurrencyBalance,
233
+ userlaunchpadGalaBalance,
234
+ userCurrencyBalance
235
+ );
236
+
237
+ const buyDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("500"));
238
+
239
+ buyDto.uniqueKey = randomUniqueKey();
240
+ buyDto.sign(users.testUser1.privateKey);
241
+
242
+ const expectedBuyResponse = plainToInstance(TradeResDto, {
243
+ inputQuantity: "0.00825575",
244
+ totalFees: "0",
245
+ totalTokenSold: new BigNumber("500").toString(),
246
+ outputQuantity: new BigNumber("500").toString(),
247
+ tokenName: "AUTOMATEDTESTCOIN",
248
+ tradeType: "Buy",
249
+ uniqueKey: buyDto.uniqueKey,
250
+ vaultAddress: "service|GALA$Unit$none$none$launchpad",
251
+ userAddress: "client|testUser1",
252
+ isFinalized: false,
253
+ functionName: "BuyExactToken"
254
+ });
255
+
256
+ const sellDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("50"));
257
+ sellDto.uniqueKey = randomUniqueKey();
258
+ const signedDto = sellDto.signed(users.testUser1.privateKey);
259
+
260
+ // When
261
+ const buyRes = await contract.BuyExactToken(ctx, buyDto);
262
+
263
+ const sellRes = await contract.SellExactToken(ctx, signedDto);
264
+
265
+ // Then
266
+ expect(buyRes).toEqual(transactionSuccess(expectedBuyResponse));
267
+
268
+ expect(sellRes.Status).toBe(1);
269
+ expect(sellRes.Data?.inputQuantity).toBe("50");
270
+ expect(sellRes.Data?.outputQuantity).toBe("0.00082579");
271
+
272
+ galaPurchaseQtyDefaultSupply = new BigNumber(sellRes.Data?.outputQuantity ?? 0);
273
+ });
274
+
275
+ test("Adjustable supply: Single transaction yields expected quantity for 100x scaled 1 Billion supply", async () => {
276
+ // Given
277
+ const multiplier = 100;
278
+ const inputQty = new BigNumber("50").times(multiplier);
279
+
280
+ sale = new LaunchpadSale(
281
+ vaultAddress,
282
+ currencyInstance.instanceKeyObj(),
283
+ undefined,
284
+ users.testUser1.identityKey,
285
+ undefined,
286
+ multiplier
287
+ );
288
+
289
+ const { ctx, contract } = fixture(LaunchpadContract)
290
+ .registeredUsers(users.testUser1)
291
+ .savedState(
292
+ currencyClass,
293
+ currencyInstance,
294
+ launchpadGalaClass,
295
+ launchpadGalaInstance,
296
+ sale,
297
+ salelaunchpadGalaBalance,
298
+ saleCurrencyBalance,
299
+ userlaunchpadGalaBalance,
300
+ userCurrencyBalance
301
+ );
302
+
303
+ const buyDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("500").times(multiplier));
304
+
305
+ buyDto.uniqueKey = randomUniqueKey();
306
+ buyDto.sign(users.testUser1.privateKey);
307
+
308
+ const expectedBuyResponse = plainToInstance(TradeResDto, {
309
+ inputQuantity: "0.00825575",
310
+ totalFees: "0",
311
+ totalTokenSold: new BigNumber("500").times(multiplier).toString(),
312
+ outputQuantity: new BigNumber("500").times(multiplier).toString(),
313
+ tokenName: "AUTOMATEDTESTCOIN",
314
+ tradeType: "Buy",
315
+ uniqueKey: buyDto.uniqueKey,
316
+ vaultAddress: "service|GALA$Unit$none$none$launchpad",
317
+ userAddress: "client|testUser1",
318
+ isFinalized: false,
319
+ functionName: "BuyExactToken"
320
+ });
321
+
322
+ const sellDto = new ExactTokenQuantityDto(vaultAddress, inputQty);
323
+ sellDto.uniqueKey = randomUniqueKey();
324
+ const signedDto = sellDto.signed(users.testUser1.privateKey);
325
+
326
+ // When
327
+ const buyRes = await contract.BuyExactToken(ctx, buyDto);
328
+
329
+ const sellRes = await contract.SellExactToken(ctx, signedDto);
330
+
331
+ // Then
332
+ expect(buyRes).toEqual(transactionSuccess(expectedBuyResponse));
333
+
334
+ expect(sellRes.Status).toBe(1);
335
+ expect(sellRes.Data?.inputQuantity).toBe(inputQty.toString());
336
+ expect(sellRes.Data?.outputQuantity).toBe("0.00082579");
337
+
338
+ const galaPurchaseQty100xSupply = new BigNumber(sellRes.Data?.outputQuantity ?? -1);
339
+
340
+ // Compared to the previous test where the Launchpad has the default 10 Million supply,
341
+ // We expect the Meme token Qty to scale 100x and the Gala Qty to remain the same
342
+ expect(galaPurchaseQtyDefaultSupply).toEqual(galaPurchaseQty100xSupply);
343
+ });
207
344
  });
@@ -58,7 +58,7 @@ export async function sellExactToken(
58
58
  const memeToken = sale.fetchSellingTokenInstanceKey();
59
59
 
60
60
  // Abort if the vault doesn't have enough native tokens to pay the user
61
- if (new BigNumber(sellTokenDTO.tokenQuantity).isGreaterThan(nativeTokensLeftInVault)) {
61
+ if (nativeTokensPayout.isGreaterThan(nativeTokensLeftInVault)) {
62
62
  throw new ValidationFailedError("Not enough GALA in sale contract to carry out this operation.");
63
63
  }
64
64
 
@@ -26,7 +26,7 @@ import { currency, fixture, transactionError, transactionSuccess, users } from "
26
26
  import BigNumber from "bignumber.js";
27
27
  import { plainToInstance } from "class-transformer";
28
28
 
29
- import { LaunchpadSale, NativeTokenQuantityDto, TradeResDto } from "../../api/types";
29
+ import { ExactTokenQuantityDto, LaunchpadSale, NativeTokenQuantityDto, TradeResDto } from "../../api/types";
30
30
  import { LaunchpadContract } from "../LaunchpadContract";
31
31
  import launchpadgala from "../test/launchpadgala";
32
32
 
@@ -286,4 +286,160 @@ describe("sellWithNative", () => {
286
286
 
287
287
  // todo: check writes map, verify vault balance
288
288
  });
289
+
290
+ let galaPurchaseQtyDefaultSupply: BigNumber;
291
+ let memeSaleQtyDefaultSupply: BigNumber;
292
+
293
+ test("Adjustable supply: Single transaction yields expected value for default 10 Million supply", async () => {
294
+ // Given
295
+ const multiplier = undefined;
296
+ galaPurchaseQtyDefaultSupply = new BigNumber("0.00082579");
297
+ memeSaleQtyDefaultSupply = new BigNumber("49.999949130655");
298
+
299
+ sale = new LaunchpadSale(
300
+ vaultAddress,
301
+ currencyInstance.instanceKeyObj(),
302
+ undefined,
303
+ users.testUser1.identityKey,
304
+ undefined,
305
+ multiplier
306
+ );
307
+
308
+ const { ctx, contract } = fixture(LaunchpadContract)
309
+ .registeredUsers(users.testUser1)
310
+ .savedState(
311
+ currencyClass,
312
+ currencyInstance,
313
+ launchpadGalaClass,
314
+ launchpadGalaInstance,
315
+ sale,
316
+ salelaunchpadGalaBalance,
317
+ saleCurrencyBalance,
318
+ userlaunchpadGalaBalance,
319
+ userCurrencyBalance
320
+ );
321
+
322
+ const buyDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("500"));
323
+
324
+ buyDto.uniqueKey = randomUniqueKey();
325
+ buyDto.sign(users.testUser1.privateKey);
326
+
327
+ const expectedBuyResponse = plainToInstance(TradeResDto, {
328
+ inputQuantity: "0.00825575",
329
+ totalFees: "0",
330
+ totalTokenSold: new BigNumber("500").toString(),
331
+ outputQuantity: new BigNumber("500").toString(),
332
+ tokenName: "AUTOMATEDTESTCOIN",
333
+ tradeType: "Buy",
334
+ uniqueKey: buyDto.uniqueKey,
335
+ vaultAddress: "service|GALA$Unit$none$none$launchpad",
336
+ userAddress: "client|testUser1",
337
+ isFinalized: false,
338
+ functionName: "BuyExactToken"
339
+ });
340
+
341
+ const sellDto = new NativeTokenQuantityDto(vaultAddress, galaPurchaseQtyDefaultSupply);
342
+ sellDto.uniqueKey = randomUniqueKey();
343
+ const signedDto = sellDto.signed(users.testUser1.privateKey);
344
+
345
+ // When
346
+ const buyRes = await contract.BuyExactToken(ctx, buyDto);
347
+
348
+ const sellRes = await contract.SellWithNative(ctx, signedDto);
349
+
350
+ // Then
351
+ expect(buyRes).toEqual(transactionSuccess(expectedBuyResponse));
352
+
353
+ expect(sellRes).toEqual(
354
+ transactionSuccess(
355
+ expect.objectContaining({
356
+ outputQuantity: galaPurchaseQtyDefaultSupply.toString(),
357
+ // extra precision in set constant above accounts for loss of precision
358
+ // when increased by the multiplier below.
359
+ // here, we round to the token decimal places to match the internal logic
360
+ inputQuantity: memeSaleQtyDefaultSupply.decimalPlaces(10).toString()
361
+ })
362
+ )
363
+ );
364
+ galaPurchaseQtyDefaultSupply = new BigNumber(sellRes.Data?.outputQuantity ?? 0);
365
+ });
366
+
367
+ test("Adjustable supply: Single transaction yields expected quantity for 100x scaled 1 Billion supply", async () => {
368
+ // Given
369
+ const multiplier = 100;
370
+
371
+ // Same Gala purchase amount should buy 100x meme token output
372
+ const inputQty = new BigNumber(galaPurchaseQtyDefaultSupply);
373
+
374
+ sale = new LaunchpadSale(
375
+ vaultAddress,
376
+ currencyInstance.instanceKeyObj(),
377
+ undefined,
378
+ users.testUser1.identityKey,
379
+ undefined,
380
+ multiplier
381
+ );
382
+
383
+ const { ctx, contract } = fixture(LaunchpadContract)
384
+ .registeredUsers(users.testUser1)
385
+ .savedState(
386
+ currencyClass,
387
+ currencyInstance,
388
+ launchpadGalaClass,
389
+ launchpadGalaInstance,
390
+ sale,
391
+ salelaunchpadGalaBalance,
392
+ saleCurrencyBalance,
393
+ userlaunchpadGalaBalance,
394
+ userCurrencyBalance
395
+ );
396
+
397
+ const buyDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("500").times(multiplier));
398
+
399
+ buyDto.uniqueKey = randomUniqueKey();
400
+ buyDto.sign(users.testUser1.privateKey);
401
+
402
+ const expectedBuyResponse = plainToInstance(TradeResDto, {
403
+ inputQuantity: "0.00825575",
404
+ totalFees: "0",
405
+ totalTokenSold: new BigNumber("500").times(multiplier).toString(),
406
+ outputQuantity: new BigNumber("500").times(multiplier).toString(),
407
+ tokenName: "AUTOMATEDTESTCOIN",
408
+ tradeType: "Buy",
409
+ uniqueKey: buyDto.uniqueKey,
410
+ vaultAddress: "service|GALA$Unit$none$none$launchpad",
411
+ userAddress: "client|testUser1",
412
+ isFinalized: false,
413
+ functionName: "BuyExactToken"
414
+ });
415
+
416
+ const sellDto = new NativeTokenQuantityDto(vaultAddress, inputQty);
417
+ sellDto.uniqueKey = randomUniqueKey();
418
+ const signedDto = sellDto.signed(users.testUser1.privateKey);
419
+
420
+ // When
421
+ const buyRes = await contract.BuyExactToken(ctx, buyDto);
422
+
423
+ const sellRes = await contract.SellWithNative(ctx, signedDto);
424
+
425
+ // Then
426
+ expect(buyRes).toEqual(transactionSuccess(expectedBuyResponse));
427
+
428
+ expect(sellRes).toEqual(
429
+ transactionSuccess(
430
+ expect.objectContaining({
431
+ inputQuantity: memeSaleQtyDefaultSupply.times(multiplier).toString(),
432
+ outputQuantity: galaPurchaseQtyDefaultSupply.toString()
433
+ })
434
+ )
435
+ );
436
+
437
+ const galaPurchaseQty100xSupply = new BigNumber(sellRes.Data?.outputQuantity ?? -1);
438
+ const memeSaleQty100xSupply = new BigNumber(sellRes.Data?.inputQuantity ?? -1);
439
+
440
+ // Compared to the previous test where the Launchpad has the default 10 Million supply,
441
+ // We expect the Meme token Qty to scale 100x and the Gala Qty to remain the same
442
+ expect(galaPurchaseQtyDefaultSupply).toEqual(galaPurchaseQty100xSupply);
443
+ expect(memeSaleQtyDefaultSupply).toEqual(memeSaleQty100xSupply.dividedBy(multiplier));
444
+ });
289
445
  });
@@ -35,6 +35,18 @@ export async function fetchAndValidateSale(
35
35
  if (sale === undefined) {
36
36
  throw new NotFoundError("Sale record not found.");
37
37
  }
38
+
39
+ if (sale.saleStatus === SaleStatus.UPCOMING) {
40
+ if (sale.saleStartTime !== undefined && sale.saleStartTime > 0 && sale.saleStartTime < ctx.txUnixTime) {
41
+ sale.saleStatus = SaleStatus.ONGOING;
42
+ } else {
43
+ throw new DefaultError(
44
+ `Upcoming: This sale is coming soon. ` +
45
+ `${sale.saleStartTime ? "Sale starts at: " + sale.saleStartTime + " Unix time." : "Start time TBD."}`
46
+ );
47
+ }
48
+ }
49
+
38
50
  if (sale.saleStatus === SaleStatus.END) {
39
51
  throw new DefaultError("This sale has already ended.");
40
52
  }
@@ -42,9 +54,13 @@ export async function fetchAndValidateSale(
42
54
  return sale;
43
55
  }
44
56
 
45
- export function getBondingConstants() {
57
+ export function getBondingConstants(multiplier?: number) {
58
+ const exponentFactor =
59
+ multiplier && multiplier > 0
60
+ ? new Decimal("1166069000000").times(1 / multiplier)
61
+ : new Decimal("1166069000000");
46
62
  return {
47
- exponentFactor: new Decimal("1166069000000"), // exponent factor / b
63
+ exponentFactor: exponentFactor, // exponent factor / b
48
64
  euler: new Decimal("2.7182818284590452353602874713527"), // e
49
65
  decimals: new Decimal("1e+18") // scaling factor for decimals
50
66
  };