@gala-chain/launchpad 1.0.14 → 1.0.15

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gala-chain/launchpad",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "GalaChain Launchpad Chaincode",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -27,6 +27,7 @@ import {
27
27
  ArrayNotEmpty,
28
28
  IsArray,
29
29
  IsBoolean,
30
+ IsInt,
30
31
  IsNotEmpty,
31
32
  IsNumber,
32
33
  IsOptional,
@@ -115,6 +116,10 @@ export class CreateTokenSaleDTO extends SubmitCallDTO {
115
116
  @IsOptional()
116
117
  public twitterUrl?: string;
117
118
 
119
+ @IsOptional()
120
+ @IsInt()
121
+ public saleStartTime?: number;
122
+
118
123
  @IsOptional()
119
124
  @ValidateNested()
120
125
  @Type(() => ReverseBondingCurveConfigurationDto)
@@ -128,7 +133,8 @@ export class CreateTokenSaleDTO extends SubmitCallDTO {
128
133
  preBuyQuantity: BigNumber,
129
134
  tokenCollection: string,
130
135
  tokenCategory: string,
131
- reverseBondingCurveConfiguration?: ReverseBondingCurveConfigurationDto
136
+ reverseBondingCurveConfiguration?: ReverseBondingCurveConfigurationDto,
137
+ saleStartTime?: number
132
138
  ) {
133
139
  super();
134
140
  this.tokenName = tokenName;
@@ -139,6 +145,10 @@ export class CreateTokenSaleDTO extends SubmitCallDTO {
139
145
  this.tokenCollection = tokenCollection;
140
146
  this.tokenCategory = tokenCategory;
141
147
  this.reverseBondingCurveConfiguration = reverseBondingCurveConfiguration;
148
+
149
+ if (saleStartTime !== undefined) {
150
+ this.saleStartTime = saleStartTime;
151
+ }
142
152
  }
143
153
  }
144
154
 
@@ -23,12 +23,13 @@ import {
23
23
  } from "@gala-chain/api";
24
24
  import BigNumber from "bignumber.js";
25
25
  import { Exclude, Type } from "class-transformer";
26
- import { IsNotEmpty, IsOptional, IsString, ValidateNested } from "class-validator";
26
+ import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString, ValidateNested } from "class-validator";
27
27
  import { JSONSchema } from "class-validator-jsonschema";
28
28
 
29
29
  import { ReverseBondingCurveConfigurationChainObject } from "./LaunchpadDtos";
30
30
 
31
31
  export enum SaleStatus {
32
+ UPCOMING = "Upcoming",
32
33
  ONGOING = "Ongoing",
33
34
  END = "Finished"
34
35
  }
@@ -49,6 +50,10 @@ export class LaunchpadSale extends ChainObject {
49
50
  @IsNotEmpty()
50
51
  public saleStatus: SaleStatus;
51
52
 
53
+ @IsOptional()
54
+ @IsInt()
55
+ public saleStartTime?: number;
56
+
52
57
  @IsNotEmpty()
53
58
  @ValidateNested()
54
59
  @Type(() => TokenInstanceKey)
@@ -104,7 +109,7 @@ export class LaunchpadSale extends ChainObject {
104
109
  @JSONSchema({
105
110
  description: "The decimals of the selling token."
106
111
  })
107
- public static SELLING_TOKEN_DECIMALS = 18;
112
+ public static SELLING_TOKEN_DECIMALS = 9;
108
113
 
109
114
  @JSONSchema({
110
115
  description: "The decimals of the native token."
@@ -115,7 +120,8 @@ export class LaunchpadSale extends ChainObject {
115
120
  vaultAddress: UserAlias,
116
121
  sellingToken: TokenInstanceKey,
117
122
  reverseBondingCurveConfiguration: ReverseBondingCurveConfigurationChainObject | undefined,
118
- saleOwner: UserAlias
123
+ saleOwner: UserAlias,
124
+ saleStartTime?: number | undefined
119
125
  ) {
120
126
  super();
121
127
 
@@ -125,11 +131,20 @@ export class LaunchpadSale extends ChainObject {
125
131
  this.sellingToken = sellingToken;
126
132
  this.sellingTokenQuantity = "1e+7";
127
133
 
134
+ if (saleStartTime) {
135
+ this.saleStartTime = saleStartTime;
136
+ }
137
+
138
+ if (this.saleStartTime !== undefined && this.saleStartTime > 0) {
139
+ this.saleStatus = SaleStatus.UPCOMING;
140
+ } else {
141
+ this.saleStatus = SaleStatus.ONGOING;
142
+ }
143
+
128
144
  this.basePrice = new BigNumber(LaunchpadSale.BASE_PRICE);
129
145
  this.exponentFactor = new BigNumber("1166069000000");
130
146
  this.maxSupply = new BigNumber("1e+7");
131
147
  this.euler = new BigNumber("2.7182818284590452353602874713527");
132
- this.saleStatus = SaleStatus.ONGOING;
133
148
 
134
149
  const nativeTokenInstance = new TokenInstanceKey();
135
150
  nativeTokenInstance.collection = "GALA";
@@ -60,7 +60,7 @@ describe("buyWithNative", () => {
60
60
 
61
61
  launchpadGalaClass = plainToInstance(TokenClass, {
62
62
  ...launchpadgala.tokenClassPlain(),
63
- decimals: 8
63
+ decimals: LaunchpadSale.NATIVE_TOKEN_DECIMALS
64
64
  });
65
65
 
66
66
  vaultAddress = asValidUserAlias(`service|${launchpadGalaClassKey.toStringKey()}$launchpad`);
@@ -27,7 +27,13 @@ import { currency, fixture, transactionError, transactionSuccess, users } from "
27
27
  import BigNumber from "bignumber.js";
28
28
  import { plainToInstance } from "class-transformer";
29
29
 
30
- import { LaunchpadFeeConfig, LaunchpadSale, NativeTokenQuantityDto, TradeResDto } from "../../api/types";
30
+ import {
31
+ ExactTokenQuantityDto,
32
+ LaunchpadFeeConfig,
33
+ LaunchpadSale,
34
+ NativeTokenQuantityDto,
35
+ TradeResDto
36
+ } from "../../api/types";
31
37
  import { LaunchpadContract } from "../LaunchpadContract";
32
38
  import launchpadgala from "../test/launchpadgala";
33
39
 
@@ -46,13 +52,15 @@ describe("buyWithNative", () => {
46
52
 
47
53
  beforeEach(() => {
48
54
  //Given
49
- currencyClass = currency.tokenClass();
55
+ currencyClass = plainToInstance(TokenClass, {
56
+ ...currency.tokenClassPlain(),
57
+ decimals: LaunchpadSale.SELLING_TOKEN_DECIMALS
58
+ });
50
59
  currencyInstance = currency.tokenInstance();
51
- launchpadGalaClass = launchpadgala.tokenClass();
52
60
 
53
61
  launchpadGalaClass = plainToInstance(TokenClass, {
54
62
  ...launchpadgala.tokenClassPlain(),
55
- decimals: 18
63
+ decimals: LaunchpadSale.NATIVE_TOKEN_DECIMALS
56
64
  });
57
65
 
58
66
  launchpadGalaInstance = launchpadgala.tokenInstance();
@@ -154,8 +162,8 @@ describe("buyWithNative", () => {
154
162
  const expectedResponse = plainToInstance(TradeResDto, {
155
163
  inputQuantity: "150",
156
164
  totalFees: "0",
157
- totalTokenSold: "2101667.8890651635",
158
- outputQuantity: "2101667.8890651635",
165
+ totalTokenSold: "2101667.889065163",
166
+ outputQuantity: "2101667.889065163",
159
167
  tokenName: "AUTOMATEDTESTCOIN",
160
168
  tradeType: "Buy",
161
169
  vaultAddress: "service|GALA$Unit$none$none$launchpad",
@@ -200,8 +208,8 @@ describe("buyWithNative", () => {
200
208
  const expectedResponse = plainToInstance(TradeResDto, {
201
209
  inputQuantity: "1000",
202
210
  totalFees: "320",
203
- totalTokenSold: "3663321.3628130557",
204
- outputQuantity: "3663321.3628130557",
211
+ totalTokenSold: "3663321.362813055",
212
+ outputQuantity: "3663321.362813055",
205
213
  tokenName: "AUTOMATEDTESTCOIN",
206
214
  tradeType: "Buy",
207
215
  vaultAddress: "service|GALA$Unit$none$none$launchpad",
@@ -256,4 +264,91 @@ describe("buyWithNative", () => {
256
264
  )
257
265
  );
258
266
  });
267
+
268
+ it("should return inverse native tokens when buying and selling tokens", async () => {
269
+ //Given
270
+ salelaunchpadGalaBalance.subtractQuantity(new BigNumber("1e+7"), 0);
271
+ saleCurrencyBalance.addQuantity(new BigNumber("1e+7"));
272
+ userlaunchpadGalaBalance.addQuantity(new BigNumber("1e+7"));
273
+ const { ctx, contract } = fixture(LaunchpadContract)
274
+ .registeredUsers(users.testUser1)
275
+ .savedState(
276
+ currencyClass,
277
+ currencyInstance,
278
+ launchpadGalaClass,
279
+ launchpadGalaInstance,
280
+ sale,
281
+ salelaunchpadGalaBalance,
282
+ saleCurrencyBalance,
283
+ userlaunchpadGalaBalance,
284
+ userCurrencyBalance
285
+ );
286
+
287
+ const arr: string[] = [
288
+ "31.27520343",
289
+ "100.3731319",
290
+ "322.1326962",
291
+ "1033.837164",
292
+ "3317.947211",
293
+ "10648.46001",
294
+ "34174.65481",
295
+ "109678.4915",
296
+ "351996.8694",
297
+ "1129679.88999"
298
+ ];
299
+
300
+ const sellingArr: string[] = [];
301
+ const sellArr: string[] = [
302
+ "1000000.000060721",
303
+ "1000000.000017497",
304
+ "999999.999868935",
305
+ "1000000.000187964",
306
+ "999999.999879254",
307
+ "999999.999929098",
308
+ "1000000.000096442",
309
+ "999999.999800915",
310
+ "1000000.000112742",
311
+ "999999.000293124"
312
+ ];
313
+
314
+ // When - Buy tokens using native token amounts from arr
315
+ for (let i = 0; i < arr.length; i++) {
316
+ let nativeCoins = Number(arr[i]);
317
+ nativeCoins = roundToDecimal(nativeCoins, 8);
318
+
319
+ const dto = new NativeTokenQuantityDto(vaultAddress, new BigNumber(nativeCoins));
320
+ dto.uniqueKey = randomUniqueKey();
321
+ dto.sign(users.testUser1.privateKey);
322
+
323
+ const buyTokenRes = await contract.BuyWithNative(ctx, dto);
324
+ expect(buyTokenRes).toEqual(transactionSuccess());
325
+ }
326
+
327
+ // When - Sell tokens back in reverse order
328
+ for (let i = sellArr.length - 1; i >= 0; i--) {
329
+ const sellDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber(sellArr[i]));
330
+ sellDto.uniqueKey = randomUniqueKey();
331
+ sellDto.sign(users.testUser1.privateKey);
332
+
333
+ const sellTokenRes = await contract.SellExactToken(ctx, sellDto);
334
+ expect(sellTokenRes).toEqual(transactionSuccess());
335
+ sellingArr.push(sellTokenRes.Data?.outputQuantity || "0");
336
+ }
337
+
338
+ // Then - Verify sellingArr is the inverse of arr with the extra ""
339
+ const expectedSellingArr = [...arr].reverse();
340
+
341
+ // Compare each element, checking that values match up to 4 decimal places
342
+ const tolerance = new BigNumber("0.0001"); // 4 decimal places tolerance
343
+ for (let i = 1; i < sellingArr.length; i++) {
344
+ const sellingValue = new BigNumber(sellingArr[i]);
345
+ const expectedValue = new BigNumber(expectedSellingArr[i]);
346
+ const difference = sellingValue.minus(expectedValue).abs();
347
+ expect(difference.isLessThanOrEqualTo(tolerance)).toBe(true);
348
+ }
349
+ });
259
350
  });
351
+ function roundToDecimal(value, decimals) {
352
+ const factor = Math.pow(10, decimals);
353
+ return Math.round(value * factor) / factor;
354
+ }
@@ -143,7 +143,7 @@ describe("callMemeTokenOut", () => {
143
143
 
144
144
  // Then
145
145
  expect(response.Data).toMatchObject({
146
- calculatedQuantity: "458291.30295364487969",
146
+ calculatedQuantity: "458291.302953644",
147
147
  extraFees: { reverseBondingCurve: "0", transactionFees: "0.00000000" }
148
148
  });
149
149
  });
@@ -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
 
@@ -131,6 +137,23 @@ export async function createSale(
131
137
  isSaleFinalized = tradeStatus.isFinalized;
132
138
  }
133
139
 
140
+ // handling the optional saleStartTime after the preBuyQuantity allows
141
+ // creators to still optionally specify a pre-buy even when a
142
+ // sale is marked Upcoming / Coming Soon.
143
+ // Otherwise `buyWithNative` would throw an error when validating the sale is active.
144
+ if (launchpadDetails.saleStartTime !== undefined) {
145
+ launchpad.saleStartTime = launchpadDetails.saleStartTime;
146
+
147
+ // handle edge case
148
+ // if a sale was immediately sold out with a pre-buy on creation,
149
+ // do not set the ended sale back to UPCOMING
150
+ if (launchpad.saleStatus !== SaleStatus.END) {
151
+ launchpad.saleStatus = SaleStatus.UPCOMING;
152
+ }
153
+
154
+ await putChainObject(ctx, launchpad);
155
+ }
156
+
134
157
  // Return the response object
135
158
  return {
136
159
  image: launchpadDetails.tokenImage,
@@ -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
 
@@ -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
  }