@gala-chain/launchpad 1.0.9 → 1.0.11
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/CLAUDE.md +279 -0
- package/lib/package.json +2 -2
- package/lib/src/api/utils/error.js +1 -1
- package/lib/src/api/utils/error.js.map +1 -1
- package/lib/src/chaincode/launchpad/buyExactToken.js +1 -1
- package/lib/src/chaincode/launchpad/buyExactToken.js.map +1 -1
- package/lib/src/chaincode/launchpad/buyWithNative.d.ts.map +1 -1
- package/lib/src/chaincode/launchpad/buyWithNative.js +12 -5
- package/lib/src/chaincode/launchpad/buyWithNative.js.map +1 -1
- package/lib/src/chaincode/launchpad/sellExactToken.js +1 -1
- package/lib/src/chaincode/launchpad/sellExactToken.js.map +1 -1
- package/lib/src/chaincode/launchpad/sellWithNative.d.ts.map +1 -1
- package/lib/src/chaincode/launchpad/sellWithNative.js +10 -2
- package/lib/src/chaincode/launchpad/sellWithNative.js.map +1 -1
- package/lib/src/chaincode/test/launchpadgala.js +1 -1
- package/lib/src/chaincode/test/launchpadgala.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/api/utils/error.ts +1 -1
- package/src/chaincode/launchpad/buyExactToken.spec.ts +90 -20
- package/src/chaincode/launchpad/buyExactToken.ts +1 -1
- package/src/chaincode/launchpad/buyWithNative.spec.ts +99 -26
- package/src/chaincode/launchpad/buyWithNative.ts +21 -6
- package/src/chaincode/launchpad/callMemeTokenIn.spec.ts +244 -0
- package/src/chaincode/launchpad/callMemeTokenOut.spec.ts +1 -1
- package/src/chaincode/launchpad/callNativeTokenIn.spec.ts +269 -0
- package/src/chaincode/launchpad/callNativeTokenOut.spec.ts +276 -0
- package/src/chaincode/launchpad/configureLaunchpadFeeConfig.spec.ts +202 -0
- package/src/chaincode/launchpad/createSale.spec.ts +259 -0
- package/src/chaincode/launchpad/fetchSaleDetails.spec.ts +141 -0
- package/src/chaincode/launchpad/finalizeTokenAllocation.spec.ts +126 -0
- package/src/chaincode/launchpad/sellExactToken.spec.ts +284 -0
- package/src/chaincode/launchpad/sellExactToken.ts +1 -1
- package/src/chaincode/launchpad/sellWithNative.spec.ts +329 -0
- package/src/chaincode/launchpad/sellWithNative.ts +23 -6
- package/src/chaincode/test/launchpadgala.ts +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Gala Games Inc. All rights reserved.
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
TokenBalance,
|
|
17
|
+
TokenClass,
|
|
18
|
+
TokenClassKey,
|
|
19
|
+
TokenInstance,
|
|
20
|
+
UserAlias,
|
|
21
|
+
asValidUserAlias,
|
|
22
|
+
randomUniqueKey
|
|
23
|
+
} from "@gala-chain/api";
|
|
24
|
+
import { InvalidDecimalError } from "@gala-chain/chaincode";
|
|
25
|
+
import { currency, fixture, transactionError, users } from "@gala-chain/test";
|
|
26
|
+
import BigNumber from "bignumber.js";
|
|
27
|
+
import { plainToInstance } from "class-transformer";
|
|
28
|
+
|
|
29
|
+
import { ExactTokenQuantityDto, LaunchpadSale } from "../../api/types";
|
|
30
|
+
import { LaunchpadContract } from "../LaunchpadContract";
|
|
31
|
+
import launchpadgala from "../test/launchpadgala";
|
|
32
|
+
|
|
33
|
+
describe("sellExactToken", () => {
|
|
34
|
+
let currencyClass: TokenClass;
|
|
35
|
+
let currencyInstance: TokenInstance;
|
|
36
|
+
let launchpadGalaClass: TokenClass;
|
|
37
|
+
let launchpadGalaInstance: TokenInstance;
|
|
38
|
+
let launchpadGalaClassKey: TokenClassKey;
|
|
39
|
+
let vaultAddress: UserAlias;
|
|
40
|
+
let sale: LaunchpadSale;
|
|
41
|
+
let salelaunchpadGalaBalance: TokenBalance;
|
|
42
|
+
let saleCurrencyBalance: TokenBalance;
|
|
43
|
+
let userlaunchpadGalaBalance: TokenBalance;
|
|
44
|
+
let userCurrencyBalance: TokenBalance;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
currencyClass = currency.tokenClass();
|
|
48
|
+
currencyInstance = currency.tokenInstance();
|
|
49
|
+
launchpadGalaClass = launchpadgala.tokenClass();
|
|
50
|
+
launchpadGalaInstance = launchpadgala.tokenInstance();
|
|
51
|
+
launchpadGalaClassKey = launchpadgala.tokenClassKey();
|
|
52
|
+
|
|
53
|
+
vaultAddress = asValidUserAlias(`service|${launchpadGalaClassKey.toStringKey()}$launchpad`);
|
|
54
|
+
|
|
55
|
+
// Initialize sale with manual values
|
|
56
|
+
sale = new LaunchpadSale(
|
|
57
|
+
vaultAddress,
|
|
58
|
+
currencyInstance.instanceKeyObj(),
|
|
59
|
+
undefined,
|
|
60
|
+
users.testUser1.identityKey
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Create sale balances - sale needs tokens to pay out
|
|
64
|
+
salelaunchpadGalaBalance = plainToInstance(TokenBalance, {
|
|
65
|
+
...launchpadgala.tokenBalance(),
|
|
66
|
+
owner: vaultAddress,
|
|
67
|
+
quantity: new BigNumber("1000000") // Large amount in vault
|
|
68
|
+
});
|
|
69
|
+
saleCurrencyBalance = plainToInstance(TokenBalance, {
|
|
70
|
+
...currency.tokenBalance(),
|
|
71
|
+
owner: vaultAddress,
|
|
72
|
+
quantity: new BigNumber("1000000") // Large amount in vault
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Create user balances - user needs tokens to sell
|
|
76
|
+
userlaunchpadGalaBalance = plainToInstance(TokenBalance, {
|
|
77
|
+
...launchpadgala.tokenBalance(),
|
|
78
|
+
owner: users.testUser1.identityKey,
|
|
79
|
+
quantity: new BigNumber("10000") // User has tokens to sell
|
|
80
|
+
});
|
|
81
|
+
userCurrencyBalance = plainToInstance(TokenBalance, {
|
|
82
|
+
...currency.tokenBalance(),
|
|
83
|
+
owner: users.testUser1.identityKey,
|
|
84
|
+
quantity: new BigNumber("1000") // User has some CURRENCY tokens
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should sell exact token amount successfully", async () => {
|
|
89
|
+
// Given
|
|
90
|
+
sale.buyToken(new BigNumber("1000"), new BigNumber("50")); // Users bought tokens, sale now has GALA
|
|
91
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
92
|
+
.registeredUsers(users.testUser1)
|
|
93
|
+
.savedState(
|
|
94
|
+
currencyClass,
|
|
95
|
+
currencyInstance,
|
|
96
|
+
launchpadGalaClass,
|
|
97
|
+
launchpadGalaInstance,
|
|
98
|
+
sale,
|
|
99
|
+
salelaunchpadGalaBalance,
|
|
100
|
+
saleCurrencyBalance,
|
|
101
|
+
userlaunchpadGalaBalance,
|
|
102
|
+
userCurrencyBalance
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const sellDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("100"));
|
|
106
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
107
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
108
|
+
|
|
109
|
+
// When
|
|
110
|
+
const response = await contract.SellExactToken(ctx, signedDto);
|
|
111
|
+
|
|
112
|
+
// Then
|
|
113
|
+
expect(response.Status).toBe(1);
|
|
114
|
+
expect(response.Data).toHaveProperty("outputQuantity");
|
|
115
|
+
expect(response.Data).toHaveProperty("inputQuantity", "100");
|
|
116
|
+
expect(response.Data).toHaveProperty("isFinalized");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should reject sell when native token has 0 decimals and bonding curve produces fractional quantity", async () => {
|
|
120
|
+
// Given
|
|
121
|
+
const zeroDecimalNativeClass = plainToInstance(TokenClass, {
|
|
122
|
+
...launchpadgala.tokenClassPlain(),
|
|
123
|
+
decimals: 0 // Integer-only native token
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Simulate prior buys to establish sale state
|
|
127
|
+
sale.buyToken(new BigNumber("5000"), new BigNumber("100"));
|
|
128
|
+
|
|
129
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
130
|
+
.registeredUsers(users.testUser1)
|
|
131
|
+
.savedState(
|
|
132
|
+
currencyClass,
|
|
133
|
+
currencyInstance,
|
|
134
|
+
zeroDecimalNativeClass,
|
|
135
|
+
launchpadGalaInstance,
|
|
136
|
+
sale,
|
|
137
|
+
salelaunchpadGalaBalance,
|
|
138
|
+
saleCurrencyBalance,
|
|
139
|
+
userlaunchpadGalaBalance,
|
|
140
|
+
userCurrencyBalance
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Choose a token quantity that will produce fractional native tokens from bonding curve
|
|
144
|
+
const sellDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("100"));
|
|
145
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
146
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
147
|
+
|
|
148
|
+
// When
|
|
149
|
+
const response = await contract.SellExactToken(ctx, signedDto);
|
|
150
|
+
|
|
151
|
+
// Then - Expect error due to decimal precision mismatch
|
|
152
|
+
expect(response).toEqual(
|
|
153
|
+
transactionError(new InvalidDecimalError(new BigNumber("0.00166022"), zeroDecimalNativeClass.decimals))
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should reject sell when meme token has 0 decimals and input dto contains greater fractional precision", async () => {
|
|
158
|
+
// Given
|
|
159
|
+
const zeroDecimalMemeClass = plainToInstance(TokenClass, {
|
|
160
|
+
...currency.tokenClassPlain(),
|
|
161
|
+
decimals: 0 // Integer-only native token
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Simulate prior buys to establish sale state
|
|
165
|
+
sale.buyToken(new BigNumber("5000"), new BigNumber("100"));
|
|
166
|
+
|
|
167
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
168
|
+
.registeredUsers(users.testUser1)
|
|
169
|
+
.savedState(
|
|
170
|
+
zeroDecimalMemeClass,
|
|
171
|
+
currencyInstance,
|
|
172
|
+
launchpadGalaClass,
|
|
173
|
+
launchpadGalaInstance,
|
|
174
|
+
sale,
|
|
175
|
+
salelaunchpadGalaBalance,
|
|
176
|
+
saleCurrencyBalance,
|
|
177
|
+
userlaunchpadGalaBalance,
|
|
178
|
+
userCurrencyBalance
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Choose a token quantity that will produce fractional native tokens from bonding curve
|
|
182
|
+
const sellDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("100.555"));
|
|
183
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
184
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
185
|
+
|
|
186
|
+
// When
|
|
187
|
+
const response = await contract.SellExactToken(ctx, signedDto);
|
|
188
|
+
|
|
189
|
+
// Then - Expect error due to decimal precision mismatch
|
|
190
|
+
expect(response).toEqual(
|
|
191
|
+
transactionError(new InvalidDecimalError(sellDto.tokenQuantity, zeroDecimalMemeClass.decimals))
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should handle small token sell amount", async () => {
|
|
196
|
+
// Given
|
|
197
|
+
sale.buyToken(new BigNumber("500"), new BigNumber("25")); // Users bought tokens, sale now has GALA
|
|
198
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
199
|
+
.registeredUsers(users.testUser1)
|
|
200
|
+
.savedState(
|
|
201
|
+
currencyClass,
|
|
202
|
+
currencyInstance,
|
|
203
|
+
launchpadGalaClass,
|
|
204
|
+
launchpadGalaInstance,
|
|
205
|
+
sale,
|
|
206
|
+
salelaunchpadGalaBalance,
|
|
207
|
+
saleCurrencyBalance,
|
|
208
|
+
userlaunchpadGalaBalance,
|
|
209
|
+
userCurrencyBalance
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const sellDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("1"));
|
|
213
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
214
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
215
|
+
|
|
216
|
+
// When
|
|
217
|
+
const response = await contract.SellExactToken(ctx, signedDto);
|
|
218
|
+
|
|
219
|
+
// Then
|
|
220
|
+
expect(response.Status).toBe(1);
|
|
221
|
+
expect(response.Data?.inputQuantity).toBe("1");
|
|
222
|
+
expect(new BigNumber(response.Data?.outputQuantity || "0").isPositive()).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should handle sell with expected native token parameter", async () => {
|
|
226
|
+
// Given
|
|
227
|
+
sale.buyToken(new BigNumber("800"), new BigNumber("40")); // Users bought tokens, sale now has GALA
|
|
228
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
229
|
+
.registeredUsers(users.testUser1)
|
|
230
|
+
.savedState(
|
|
231
|
+
currencyClass,
|
|
232
|
+
currencyInstance,
|
|
233
|
+
launchpadGalaClass,
|
|
234
|
+
launchpadGalaInstance,
|
|
235
|
+
sale,
|
|
236
|
+
salelaunchpadGalaBalance,
|
|
237
|
+
saleCurrencyBalance,
|
|
238
|
+
userlaunchpadGalaBalance,
|
|
239
|
+
userCurrencyBalance
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const sellDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("50"));
|
|
243
|
+
sellDto.expectedNativeToken = new BigNumber("0.0001"); // Set realistic expectation for slippage protection
|
|
244
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
245
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
246
|
+
|
|
247
|
+
// When
|
|
248
|
+
const response = await contract.SellExactToken(ctx, signedDto);
|
|
249
|
+
|
|
250
|
+
// Then
|
|
251
|
+
expect(response.Status).toBe(1);
|
|
252
|
+
expect(response.Data?.inputQuantity).toBe("50");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should handle large token sell amount", async () => {
|
|
256
|
+
// Given
|
|
257
|
+
sale.buyToken(new BigNumber("2000"), new BigNumber("100")); // Users bought tokens, sale now has GALA
|
|
258
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
259
|
+
.registeredUsers(users.testUser1)
|
|
260
|
+
.savedState(
|
|
261
|
+
currencyClass,
|
|
262
|
+
currencyInstance,
|
|
263
|
+
launchpadGalaClass,
|
|
264
|
+
launchpadGalaInstance,
|
|
265
|
+
sale,
|
|
266
|
+
salelaunchpadGalaBalance,
|
|
267
|
+
saleCurrencyBalance,
|
|
268
|
+
userlaunchpadGalaBalance,
|
|
269
|
+
userCurrencyBalance
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const sellDto = new ExactTokenQuantityDto(vaultAddress, new BigNumber("500"));
|
|
273
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
274
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
275
|
+
|
|
276
|
+
// When
|
|
277
|
+
const response = await contract.SellExactToken(ctx, signedDto);
|
|
278
|
+
|
|
279
|
+
// Then
|
|
280
|
+
expect(response.Status).toBe(1);
|
|
281
|
+
expect(response.Data?.inputQuantity).toBe("500");
|
|
282
|
+
expect(new BigNumber(response.Data?.outputQuantity || "0").isPositive()).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -67,7 +67,7 @@ export async function sellExactToken(
|
|
|
67
67
|
sellTokenDTO.expectedNativeToken.comparedTo(nativeTokensToProvide) > 0
|
|
68
68
|
) {
|
|
69
69
|
throw new SlippageToleranceExceededError(
|
|
70
|
-
|
|
70
|
+
`expected ${sellTokenDTO.expectedNativeToken.toString()}, but only ${nativeTokensToProvide.toString()} tokens can be provided. Reduce the expected amount or adjust your slippage tolerance.`
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Gala Games Inc. All rights reserved.
|
|
3
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License.
|
|
5
|
+
* You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
* See the License for the specific language governing permissions and
|
|
13
|
+
* limitations under the License.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
TokenBalance,
|
|
17
|
+
TokenClass,
|
|
18
|
+
TokenClassKey,
|
|
19
|
+
TokenInstance,
|
|
20
|
+
UserAlias,
|
|
21
|
+
asValidUserAlias,
|
|
22
|
+
randomUniqueKey
|
|
23
|
+
} from "@gala-chain/api";
|
|
24
|
+
import { InvalidDecimalError } from "@gala-chain/chaincode";
|
|
25
|
+
import { currency, fixture, transactionError, transactionSuccess, users } from "@gala-chain/test";
|
|
26
|
+
import BigNumber from "bignumber.js";
|
|
27
|
+
import { plainToInstance } from "class-transformer";
|
|
28
|
+
|
|
29
|
+
import { LaunchpadSale, NativeTokenQuantityDto, TradeResDto } from "../../api/types";
|
|
30
|
+
import { LaunchpadContract } from "../LaunchpadContract";
|
|
31
|
+
import launchpadgala from "../test/launchpadgala";
|
|
32
|
+
|
|
33
|
+
describe("sellWithNative", () => {
|
|
34
|
+
let currencyClass: TokenClass;
|
|
35
|
+
let currencyInstance: TokenInstance;
|
|
36
|
+
let launchpadGalaClass: TokenClass;
|
|
37
|
+
let launchpadGalaInstance: TokenInstance;
|
|
38
|
+
let launchpadGalaClassKey: TokenClassKey;
|
|
39
|
+
let vaultAddress: UserAlias;
|
|
40
|
+
let sale: LaunchpadSale;
|
|
41
|
+
let salelaunchpadGalaBalance: TokenBalance;
|
|
42
|
+
let saleCurrencyBalance: TokenBalance;
|
|
43
|
+
let userlaunchpadGalaBalance: TokenBalance;
|
|
44
|
+
let userCurrencyBalance: TokenBalance;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
currencyClass = currency.tokenClass();
|
|
48
|
+
currencyInstance = currency.tokenInstance();
|
|
49
|
+
launchpadGalaClass = launchpadgala.tokenClass();
|
|
50
|
+
launchpadGalaInstance = launchpadgala.tokenInstance();
|
|
51
|
+
launchpadGalaClassKey = launchpadgala.tokenClassKey();
|
|
52
|
+
|
|
53
|
+
vaultAddress = asValidUserAlias(`service|${launchpadGalaClassKey.toStringKey()}$launchpad`);
|
|
54
|
+
|
|
55
|
+
// Initialize sale with manual values
|
|
56
|
+
sale = new LaunchpadSale(
|
|
57
|
+
vaultAddress,
|
|
58
|
+
currencyInstance.instanceKeyObj(),
|
|
59
|
+
undefined,
|
|
60
|
+
users.testUser1.identityKey
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Create sale balances - sale needs tokens to pay out
|
|
64
|
+
salelaunchpadGalaBalance = plainToInstance(TokenBalance, {
|
|
65
|
+
...launchpadgala.tokenBalance(),
|
|
66
|
+
owner: vaultAddress,
|
|
67
|
+
quantity: new BigNumber("1000000") // Large amount in vault
|
|
68
|
+
});
|
|
69
|
+
saleCurrencyBalance = plainToInstance(TokenBalance, {
|
|
70
|
+
...currency.tokenBalance(),
|
|
71
|
+
owner: vaultAddress,
|
|
72
|
+
quantity: new BigNumber("1000000") // Large amount in vault
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Create user balances - user needs tokens to sell
|
|
76
|
+
userlaunchpadGalaBalance = plainToInstance(TokenBalance, {
|
|
77
|
+
...launchpadgala.tokenBalance(),
|
|
78
|
+
owner: users.testUser1.identityKey,
|
|
79
|
+
quantity: new BigNumber("10000") // User has tokens to sell
|
|
80
|
+
});
|
|
81
|
+
userCurrencyBalance = plainToInstance(TokenBalance, {
|
|
82
|
+
...currency.tokenBalance(),
|
|
83
|
+
owner: users.testUser1.identityKey,
|
|
84
|
+
quantity: new BigNumber("10000") // User has some CURRENCY tokens
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("Round sell qty when bonding curve produces fractional precision than meme TokenClass.decimals", async () => {
|
|
89
|
+
// Given - Setup meme token with 0 decimals to force decimal precision error
|
|
90
|
+
const zeroDecimalMemeTokenClass = plainToInstance(TokenClass, {
|
|
91
|
+
...currency.tokenClassPlain(),
|
|
92
|
+
decimals: 0 // Integer-only meme token
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Simulate prior buys to establish sale state with native tokens
|
|
96
|
+
sale.buyToken(new BigNumber("10000"), new BigNumber("10"));
|
|
97
|
+
|
|
98
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
99
|
+
.registeredUsers(users.testUser1)
|
|
100
|
+
.savedState(
|
|
101
|
+
currencyInstance,
|
|
102
|
+
zeroDecimalMemeTokenClass,
|
|
103
|
+
launchpadGalaClass,
|
|
104
|
+
launchpadGalaInstance,
|
|
105
|
+
sale,
|
|
106
|
+
salelaunchpadGalaBalance,
|
|
107
|
+
saleCurrencyBalance,
|
|
108
|
+
userlaunchpadGalaBalance,
|
|
109
|
+
userCurrencyBalance
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Request native tokens that will require fractional meme tokens from bonding curve
|
|
113
|
+
const sellDto = new NativeTokenQuantityDto(vaultAddress, new BigNumber("0.001"));
|
|
114
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
115
|
+
sellDto.sign(users.testUser1.privateKey);
|
|
116
|
+
|
|
117
|
+
// When
|
|
118
|
+
const response = await contract.SellWithNative(ctx, sellDto);
|
|
119
|
+
|
|
120
|
+
// Then - Expect error due to decimal precision mismatch
|
|
121
|
+
expect(response).not.toEqual(
|
|
122
|
+
transactionError(
|
|
123
|
+
new InvalidDecimalError(new BigNumber("9940.1186641108"), zeroDecimalMemeTokenClass.decimals)
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should reject sell when dto contains fractional precision greater than native TokenClass.decimals", async () => {
|
|
129
|
+
// Given - Setup meme token with 0 decimals to force decimal precision error
|
|
130
|
+
const zeroDecimalLaunchpadGalaClass = plainToInstance(TokenClass, {
|
|
131
|
+
...launchpadgala.tokenClassPlain(),
|
|
132
|
+
decimals: 8 // This codebase currently hard-codes 8 as NATIVE_TOKEN_DECIMALS...
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Simulate prior buys to establish sale state with native tokens
|
|
136
|
+
sale.buyToken(new BigNumber("100"), new BigNumber("100"));
|
|
137
|
+
|
|
138
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
139
|
+
.registeredUsers(users.testUser1)
|
|
140
|
+
.savedState(
|
|
141
|
+
currencyClass,
|
|
142
|
+
currencyInstance,
|
|
143
|
+
zeroDecimalLaunchpadGalaClass,
|
|
144
|
+
launchpadGalaInstance,
|
|
145
|
+
sale,
|
|
146
|
+
salelaunchpadGalaBalance,
|
|
147
|
+
saleCurrencyBalance,
|
|
148
|
+
userlaunchpadGalaBalance,
|
|
149
|
+
userCurrencyBalance
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Request native tokens that will require fractional meme tokens from bonding curve
|
|
153
|
+
const sellDto = new NativeTokenQuantityDto(vaultAddress, new BigNumber("0.123456789"));
|
|
154
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
155
|
+
sellDto.sign(users.testUser1.privateKey);
|
|
156
|
+
|
|
157
|
+
// When
|
|
158
|
+
const response = await contract.SellWithNative(ctx, sellDto);
|
|
159
|
+
|
|
160
|
+
// Then - Expect error due to decimal precision mismatch
|
|
161
|
+
expect(response).toEqual(
|
|
162
|
+
transactionError(
|
|
163
|
+
new InvalidDecimalError(sellDto.nativeTokenQuantity, zeroDecimalLaunchpadGalaClass.decimals)
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should sell tokens for native currency successfully", async () => {
|
|
169
|
+
// Given
|
|
170
|
+
sale.buyToken(new BigNumber("10000"), new BigNumber("10")); // Users bought tokens, sale now has GALA
|
|
171
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
172
|
+
.registeredUsers(users.testUser1)
|
|
173
|
+
.savedState(
|
|
174
|
+
currencyClass,
|
|
175
|
+
currencyInstance,
|
|
176
|
+
launchpadGalaClass,
|
|
177
|
+
launchpadGalaInstance,
|
|
178
|
+
sale,
|
|
179
|
+
salelaunchpadGalaBalance,
|
|
180
|
+
saleCurrencyBalance,
|
|
181
|
+
userlaunchpadGalaBalance,
|
|
182
|
+
userCurrencyBalance
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const sellDto = new NativeTokenQuantityDto(vaultAddress, new BigNumber("0.1"));
|
|
186
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
187
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
188
|
+
|
|
189
|
+
const expectedResponse = plainToInstance(TradeResDto, {
|
|
190
|
+
functionName: "SellWithNative",
|
|
191
|
+
inputQuantity: "6008.9271949682",
|
|
192
|
+
isFinalized: false,
|
|
193
|
+
outputQuantity: "0.1",
|
|
194
|
+
tokenName: "AUTOMATEDTESTCOIN",
|
|
195
|
+
totalFees: "0",
|
|
196
|
+
totalTokenSold: "3991.0728050318",
|
|
197
|
+
tradeType: "Sell",
|
|
198
|
+
uniqueKey: sellDto.uniqueKey,
|
|
199
|
+
userAddress: "client|testUser1",
|
|
200
|
+
vaultAddress: "service|GALA$Unit$none$none$launchpad"
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// When
|
|
204
|
+
const response = await contract.SellWithNative(ctx, signedDto);
|
|
205
|
+
|
|
206
|
+
// Then
|
|
207
|
+
expect(response).toEqual(transactionSuccess(expectedResponse));
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should handle small native token sell amount", async () => {
|
|
211
|
+
// Given
|
|
212
|
+
sale.buyToken(new BigNumber("5000"), new BigNumber("5")); // Users bought tokens, sale now has GALA
|
|
213
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
214
|
+
.registeredUsers(users.testUser1)
|
|
215
|
+
.savedState(
|
|
216
|
+
currencyClass,
|
|
217
|
+
currencyInstance,
|
|
218
|
+
launchpadGalaClass,
|
|
219
|
+
launchpadGalaInstance,
|
|
220
|
+
sale,
|
|
221
|
+
salelaunchpadGalaBalance,
|
|
222
|
+
saleCurrencyBalance,
|
|
223
|
+
userlaunchpadGalaBalance,
|
|
224
|
+
userCurrencyBalance
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const sellDto = new NativeTokenQuantityDto(vaultAddress, new BigNumber("0.001"));
|
|
228
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
229
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
230
|
+
|
|
231
|
+
// When
|
|
232
|
+
const response = await contract.SellWithNative(ctx, signedDto);
|
|
233
|
+
|
|
234
|
+
// Then
|
|
235
|
+
expect(response).toEqual(transactionSuccess());
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("should handle sell with expected token parameter", async () => {
|
|
239
|
+
// Given
|
|
240
|
+
sale.buyToken(new BigNumber("8000"), new BigNumber("8")); // Users bought tokens, sale now has GALA
|
|
241
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
242
|
+
.registeredUsers(users.testUser1)
|
|
243
|
+
.savedState(
|
|
244
|
+
currencyClass,
|
|
245
|
+
currencyInstance,
|
|
246
|
+
launchpadGalaClass,
|
|
247
|
+
launchpadGalaInstance,
|
|
248
|
+
sale,
|
|
249
|
+
salelaunchpadGalaBalance,
|
|
250
|
+
saleCurrencyBalance,
|
|
251
|
+
userlaunchpadGalaBalance,
|
|
252
|
+
userCurrencyBalance
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const sellDto = new NativeTokenQuantityDto(vaultAddress, new BigNumber("0.001"));
|
|
256
|
+
|
|
257
|
+
// expectedToken value, if provided, must be greater than or equal to actual amount received
|
|
258
|
+
// in other words, if the transaction would sell the buyer less than the buyer expects, it fails.
|
|
259
|
+
sellDto.expectedToken = new BigNumber("100"); // Set expectation for slippage protection
|
|
260
|
+
|
|
261
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
262
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
263
|
+
|
|
264
|
+
// When
|
|
265
|
+
const response = await contract.SellWithNative(ctx, signedDto);
|
|
266
|
+
|
|
267
|
+
// Then
|
|
268
|
+
expect(response).toEqual(transactionSuccess());
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should handle large native token sell amount", async () => {
|
|
272
|
+
// Given
|
|
273
|
+
sale.buyToken(new BigNumber("2000000000"), new BigNumber("200000000")); // Users bought tokens, sale now has GALA
|
|
274
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
275
|
+
.registeredUsers(users.testUser1)
|
|
276
|
+
.savedState(
|
|
277
|
+
currencyClass,
|
|
278
|
+
currencyInstance,
|
|
279
|
+
launchpadGalaClass,
|
|
280
|
+
launchpadGalaInstance,
|
|
281
|
+
sale,
|
|
282
|
+
salelaunchpadGalaBalance,
|
|
283
|
+
saleCurrencyBalance,
|
|
284
|
+
userlaunchpadGalaBalance,
|
|
285
|
+
userCurrencyBalance
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const sellDto = new NativeTokenQuantityDto(vaultAddress, new BigNumber("1000"));
|
|
289
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
290
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
291
|
+
|
|
292
|
+
// When
|
|
293
|
+
const response = await contract.SellWithNative(ctx, signedDto);
|
|
294
|
+
|
|
295
|
+
// Then
|
|
296
|
+
expect(response).toEqual(transactionSuccess());
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should handle edge case with vault balance limits", async () => {
|
|
300
|
+
// Given
|
|
301
|
+
sale.buyToken(new BigNumber("10000"), new BigNumber("1000")); // Users bought tokens, sale has GALA amount
|
|
302
|
+
const { ctx, contract } = fixture(LaunchpadContract)
|
|
303
|
+
.registeredUsers(users.testUser1)
|
|
304
|
+
.savedState(
|
|
305
|
+
currencyClass,
|
|
306
|
+
currencyInstance,
|
|
307
|
+
launchpadGalaClass,
|
|
308
|
+
launchpadGalaInstance,
|
|
309
|
+
sale,
|
|
310
|
+
salelaunchpadGalaBalance,
|
|
311
|
+
saleCurrencyBalance,
|
|
312
|
+
userlaunchpadGalaBalance,
|
|
313
|
+
userCurrencyBalance
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Try to sell amount that exceeds what's in the vault
|
|
317
|
+
const sellDto = new NativeTokenQuantityDto(vaultAddress, new BigNumber("0.1"));
|
|
318
|
+
sellDto.uniqueKey = randomUniqueKey();
|
|
319
|
+
const signedDto = sellDto.signed(users.testUser1.privateKey);
|
|
320
|
+
|
|
321
|
+
// When
|
|
322
|
+
const response = await contract.SellWithNative(ctx, signedDto);
|
|
323
|
+
|
|
324
|
+
// Then
|
|
325
|
+
expect(response).toEqual(transactionSuccess());
|
|
326
|
+
|
|
327
|
+
// todo: check writes map, verify vault balance
|
|
328
|
+
});
|
|
329
|
+
});
|
|
@@ -12,11 +12,17 @@
|
|
|
12
12
|
* See the License for the specific language governing permissions and
|
|
13
13
|
* limitations under the License.
|
|
14
14
|
*/
|
|
15
|
-
import { ValidationFailedError } from "@gala-chain/api";
|
|
16
|
-
import {
|
|
15
|
+
import { TokenClass, ValidationFailedError } from "@gala-chain/api";
|
|
16
|
+
import {
|
|
17
|
+
GalaChainContext,
|
|
18
|
+
fetchTokenClass,
|
|
19
|
+
getObjectByKey,
|
|
20
|
+
putChainObject,
|
|
21
|
+
transferToken
|
|
22
|
+
} from "@gala-chain/chaincode";
|
|
17
23
|
import BigNumber from "bignumber.js";
|
|
18
24
|
|
|
19
|
-
import { NativeTokenQuantityDto, TradeResDto } from "../../api/types";
|
|
25
|
+
import { LaunchpadSale, NativeTokenQuantityDto, TradeResDto } from "../../api/types";
|
|
20
26
|
import { SlippageToleranceExceededError } from "../../api/utils/error";
|
|
21
27
|
import { fetchAndValidateSale, fetchLaunchpadFeeAddress } from "../utils";
|
|
22
28
|
import { callMemeTokenIn } from "./callMemeTokenIn";
|
|
@@ -46,7 +52,16 @@ export async function sellWithNative(
|
|
|
46
52
|
sellTokenDTO: NativeTokenQuantityDto
|
|
47
53
|
): Promise<TradeResDto> {
|
|
48
54
|
// Fetch and validate the sale object
|
|
49
|
-
const sale = await fetchAndValidateSale(ctx, sellTokenDTO.vaultAddress);
|
|
55
|
+
const sale: LaunchpadSale = await fetchAndValidateSale(ctx, sellTokenDTO.vaultAddress);
|
|
56
|
+
|
|
57
|
+
const { collection, category, type, additionalKey } = sale.sellingToken;
|
|
58
|
+
const sellingTokenCompositeKey = TokenClass.getCompositeKeyFromParts(TokenClass.INDEX_KEY, [
|
|
59
|
+
collection,
|
|
60
|
+
category,
|
|
61
|
+
type,
|
|
62
|
+
additionalKey
|
|
63
|
+
]);
|
|
64
|
+
const sellingToken = await getObjectByKey(ctx, TokenClass, sellingTokenCompositeKey);
|
|
50
65
|
|
|
51
66
|
const nativeTokensLeftInVault = new BigNumber(sale.nativeTokenQuantity);
|
|
52
67
|
|
|
@@ -58,7 +73,9 @@ export async function sellWithNative(
|
|
|
58
73
|
// Calculate how many tokens need to be sold to get the requested native amount
|
|
59
74
|
const callMemeTokenInResult = await callMemeTokenIn(ctx, sellTokenDTO);
|
|
60
75
|
const transactionFees = callMemeTokenInResult.extraFees.transactionFees;
|
|
61
|
-
const tokensToSell = new BigNumber(callMemeTokenInResult.calculatedQuantity)
|
|
76
|
+
const tokensToSell = new BigNumber(callMemeTokenInResult.calculatedQuantity).decimalPlaces(
|
|
77
|
+
sellingToken.decimals
|
|
78
|
+
);
|
|
62
79
|
|
|
63
80
|
const nativeToken = sale.fetchNativeTokenInstanceKey();
|
|
64
81
|
const memeToken = sale.fetchSellingTokenInstanceKey();
|
|
@@ -66,7 +83,7 @@ export async function sellWithNative(
|
|
|
66
83
|
// Enforce slippage tolerance
|
|
67
84
|
if (sellTokenDTO.expectedToken && sellTokenDTO.expectedToken.comparedTo(tokensToSell) < 0) {
|
|
68
85
|
throw new SlippageToleranceExceededError(
|
|
69
|
-
|
|
86
|
+
`expected ${sellTokenDTO.expectedToken.toString()}, but at least ${tokensToSell.toString()} tokens are required. Increase the expected amount or adjust your slippage tolerance.`
|
|
70
87
|
);
|
|
71
88
|
}
|
|
72
89
|
|
|
@@ -41,7 +41,7 @@ const tokenClassKeyPlain = createPlainFn({
|
|
|
41
41
|
const tokenClassPlain = createPlainFn({
|
|
42
42
|
...tokenClassKeyPlain(),
|
|
43
43
|
description: "Generated via automated test suite.",
|
|
44
|
-
decimals:
|
|
44
|
+
decimals: 8, // This codebase currently hard-codes 8 as NATIVE_TOKEN_DECIMALS
|
|
45
45
|
image: "https://app.gala.games/test-image-placeholder-url.png",
|
|
46
46
|
isNonFungible: false,
|
|
47
47
|
maxCapacity: new BigNumber(100000000000000),
|