@aurigami/sdk 1.2.5 → 1.3.0
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/README.md +5 -0
- package/dist/airdrop-lottery.d.ts +14 -0
- package/dist/constants.d.ts +3 -0
- package/dist/helpers.d.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/sdk.cjs.development.js +530 -0
- package/dist/sdk.cjs.development.js.map +1 -1
- package/dist/sdk.cjs.production.min.js +1 -1
- package/dist/sdk.cjs.production.min.js.map +1 -1
- package/dist/sdk.esm.js +523 -1
- package/dist/sdk.esm.js.map +1 -1
- package/dist/transfer-event-query.d.ts +17 -0
- package/package.json +1 -1
- package/src/airdrop-lottery.ts +232 -0
- package/src/constants.ts +6 -1
- package/src/helpers.ts +54 -1
- package/src/index.ts +2 -0
- package/src/transfer-event-query.ts +60 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BlockchainEntityRead } from "./BlockchainEntity";
|
|
2
|
+
import { NetworkConnection } from "./types";
|
|
3
|
+
import { AuToken } from "@aurigami/contracts/typechain";
|
|
4
|
+
import { BigNumber as BN } from "ethers";
|
|
5
|
+
declare type SimpleTransfer = {
|
|
6
|
+
from: string;
|
|
7
|
+
to: string;
|
|
8
|
+
amount: BN;
|
|
9
|
+
blockNumber: number;
|
|
10
|
+
};
|
|
11
|
+
export declare class TransferEventQuery extends BlockchainEntityRead {
|
|
12
|
+
auToken: AuToken;
|
|
13
|
+
constructor(tokenAddr: string, networkConnection: NetworkConnection);
|
|
14
|
+
queryERC20TransfersOf(address: string, fromBlock: number, toBlock: number): Promise<SimpleTransfer[]>;
|
|
15
|
+
queryERC20BalanceAt(address: string, endBlock: number): Promise<BN>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
package/package.json
CHANGED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Provider } from "@ethersproject/abstract-provider";
|
|
2
|
+
import { BlockchainEntity, BlockchainEntityRead } from "./BlockchainEntity";
|
|
3
|
+
import { NetworkConnection } from "./types";
|
|
4
|
+
import * as FileSystem from 'fs';
|
|
5
|
+
import { BigNumber as BN } from "ethers";
|
|
6
|
+
import { TransferEventQuery } from "./transfer-event-query";
|
|
7
|
+
import { AIRDROP_START_TIMESTAMP, AIRDROP_KEEP_THRESHOLD, AIRDROP_END_TIMESTAMP } from "./constants";
|
|
8
|
+
import { getBlockBeforeTimestamp, getCurrentTimestamp } from "./helpers";
|
|
9
|
+
|
|
10
|
+
type AirdropUnderlyingInfo = {
|
|
11
|
+
minDeposit: BN,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type AirdropAuTokenInfo = {
|
|
15
|
+
address: string,
|
|
16
|
+
exchangeRate: BN,
|
|
17
|
+
deploymentBlock: number,
|
|
18
|
+
underlying: AirdropUnderlyingInfo
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class AirdropSettings {
|
|
22
|
+
private static _instance: AirdropSettings;
|
|
23
|
+
|
|
24
|
+
airdropStartBlock: number = 0;
|
|
25
|
+
airdropEndBlock: number = 0;
|
|
26
|
+
airdropStartTimestamp: number = AIRDROP_START_TIMESTAMP;
|
|
27
|
+
airdropEndTimestamp: number = AIRDROP_END_TIMESTAMP;
|
|
28
|
+
airdropThreshold: number = AIRDROP_KEEP_THRESHOLD;
|
|
29
|
+
private constructor() {}
|
|
30
|
+
|
|
31
|
+
public static async getSettings(provider: Provider): Promise<AirdropSettings> {
|
|
32
|
+
if (this._instance)
|
|
33
|
+
return this._instance;
|
|
34
|
+
|
|
35
|
+
let settings: AirdropSettings = new AirdropSettings();
|
|
36
|
+
|
|
37
|
+
settings.airdropStartBlock = await getBlockBeforeTimestamp(
|
|
38
|
+
provider,
|
|
39
|
+
settings.airdropStartTimestamp,
|
|
40
|
+
63317969, //block number at the time I was writing this line, it's sure to be before the start of the airdrop
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
settings.airdropEndBlock = await getBlockBeforeTimestamp(
|
|
44
|
+
provider,
|
|
45
|
+
settings.airdropEndTimestamp,
|
|
46
|
+
settings.airdropStartBlock,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
this._instance = settings;
|
|
50
|
+
return settings;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class AirdropAuTokenKeepTracker extends BlockchainEntityRead {
|
|
55
|
+
private _transferQuery: TransferEventQuery;
|
|
56
|
+
private _airdropAuTokenInfo: AirdropAuTokenInfo;
|
|
57
|
+
public constructor(networkConnection: NetworkConnection, airdropAuTokenInfo: AirdropAuTokenInfo) {
|
|
58
|
+
super(networkConnection);
|
|
59
|
+
this._transferQuery = new TransferEventQuery(airdropAuTokenInfo.address, networkConnection);
|
|
60
|
+
this._airdropAuTokenInfo = airdropAuTokenInfo;
|
|
61
|
+
|
|
62
|
+
// exchangeRate = 102% * exchangeRate, see why in the comment of _toUnderlyingValue
|
|
63
|
+
this._airdropAuTokenInfo.exchangeRate = this._airdropAuTokenInfo.exchangeRate.add(
|
|
64
|
+
this._airdropAuTokenInfo.exchangeRate.div(50)
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert the given amount of auToken to the underlying value with the stored exchange rate
|
|
70
|
+
*
|
|
71
|
+
* Why we need to do this?
|
|
72
|
+
*
|
|
73
|
+
* By transfer event, we cannot know the exact underlying, the only
|
|
74
|
+
* info that we can keep track of via transfer is event is the auToken amount. So we need to
|
|
75
|
+
* convert it to the underlying value with the stored exchange rate.
|
|
76
|
+
*
|
|
77
|
+
* However, we cannot just simply use auTokenAmount * exchangeRate, because the exchange rate
|
|
78
|
+
* is changing over time. As advice of the contract team, use fixed exchangeRate * 102% is good enough.
|
|
79
|
+
*
|
|
80
|
+
*/
|
|
81
|
+
private _toUnderlyingValue(auTokenAmount: BN): BN {
|
|
82
|
+
return auTokenAmount.mul(
|
|
83
|
+
this._airdropAuTokenInfo.exchangeRate
|
|
84
|
+
).div(BN.from(10).pow(18));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if the user has been deposit at least (minDeposit) and keep it for
|
|
89
|
+
* more than AIRDROP_KEEP_THRESHOLD milliseconds
|
|
90
|
+
*
|
|
91
|
+
* How it works:
|
|
92
|
+
* Whenever the user balance is exceed minDeposit, we will record the timestamp,
|
|
93
|
+
* and when the balance is below minDeposit, we will compare with the saved timestamp to see
|
|
94
|
+
* whether it has been keep for more than AIRDROP_KEEP_THRESHOLD milliseconds.
|
|
95
|
+
*
|
|
96
|
+
*/
|
|
97
|
+
public async isEligible(address: string): Promise<boolean> {
|
|
98
|
+
const settings = await AirdropSettings.getSettings(this._networkConnection.provider);
|
|
99
|
+
const minDeposit = this._airdropAuTokenInfo.underlying.minDeposit;
|
|
100
|
+
|
|
101
|
+
// Get user balance at the time of Airdrop start
|
|
102
|
+
let balance = await this._transferQuery.queryERC20BalanceAt(
|
|
103
|
+
address,
|
|
104
|
+
settings.airdropStartBlock - 1
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const [BELOW, ABOVE] = [false, true];
|
|
108
|
+
let flag = BELOW;
|
|
109
|
+
// variable to store the last timestamp that, from it to "current" timestamp,
|
|
110
|
+
// the balance is always above minDeposit
|
|
111
|
+
let lastEligibleBalanceTimestamp = 0;
|
|
112
|
+
|
|
113
|
+
if (this._toUnderlyingValue(balance).gte(minDeposit)) {
|
|
114
|
+
lastEligibleBalanceTimestamp = settings.airdropStartTimestamp;
|
|
115
|
+
flag = ABOVE;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let events = await this._transferQuery.queryERC20TransfersOf(
|
|
119
|
+
address,
|
|
120
|
+
settings.airdropStartBlock,
|
|
121
|
+
settings.airdropEndBlock
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
for (let { from, to, amount, blockNumber } of events) {
|
|
125
|
+
if (from == address) {
|
|
126
|
+
balance = balance.sub(amount);
|
|
127
|
+
}
|
|
128
|
+
if (to == address) {
|
|
129
|
+
balance = balance.add(amount);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// if it is different from the flag, that means the balance is changing
|
|
133
|
+
if (this._toUnderlyingValue(balance).gte(minDeposit) != flag) {
|
|
134
|
+
flag = !flag;
|
|
135
|
+
const timestamp = (await this._networkConnection.provider.getBlock(blockNumber)).timestamp;
|
|
136
|
+
|
|
137
|
+
// if this txn make the balance below the minDeposit,
|
|
138
|
+
// check whether it has been keep for more than AIRDROP_KEEP_THRESHOLD
|
|
139
|
+
if (flag == BELOW) {
|
|
140
|
+
if (timestamp - lastEligibleBalanceTimestamp >= settings.airdropThreshold) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
lastEligibleBalanceTimestamp = 0;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// record the timestamp when the balance is above minDeposit
|
|
147
|
+
lastEligibleBalanceTimestamp = timestamp;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// if after the last txn, the balance is still above minDeposit,
|
|
153
|
+
// we should check from the last timestamp to "now"
|
|
154
|
+
if (flag == ABOVE) {
|
|
155
|
+
const timestamp = Math.min(settings.airdropEndTimestamp, getCurrentTimestamp());
|
|
156
|
+
return timestamp - lastEligibleBalanceTimestamp >= settings.airdropThreshold;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export class AirdropRead extends BlockchainEntityRead {
|
|
164
|
+
private _whitelistedAddresses: Set<string>;
|
|
165
|
+
private keepTrackers: AirdropAuTokenKeepTracker[] = [];
|
|
166
|
+
public constructor(networkConnection: NetworkConnection) {
|
|
167
|
+
super(networkConnection);
|
|
168
|
+
this._whitelistedAddresses = new Set(
|
|
169
|
+
FileSystem.readFileSync("airdrop_misc/whitelist.txt").toString().trim().split('\n')
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
let auTokens = JSON.parse(
|
|
173
|
+
FileSystem.readFileSync("airdrop_misc/auTokensInfo.json").toString()
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
for (let auToken in auTokens) {
|
|
177
|
+
let { address, exchangeRate, deploymentBlock, underlying } = auTokens[auToken];
|
|
178
|
+
|
|
179
|
+
let airdropInfo: AirdropAuTokenInfo = {
|
|
180
|
+
address: address,
|
|
181
|
+
exchangeRate: BN.from(exchangeRate),
|
|
182
|
+
deploymentBlock: parseInt(deploymentBlock),
|
|
183
|
+
underlying: {
|
|
184
|
+
minDeposit: BN.from(underlying.minDeposit),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.keepTrackers.push(
|
|
189
|
+
new AirdropAuTokenKeepTracker(
|
|
190
|
+
networkConnection,
|
|
191
|
+
airdropInfo
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// this is for testing purpose only
|
|
198
|
+
public countWhitelistedAddresses(): number {
|
|
199
|
+
return this._whitelistedAddresses.size;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
public isWhitelisted(user: string): boolean {
|
|
203
|
+
return this._whitelistedAddresses.has(user);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
public async hasCompletedChallenge(user: string): Promise<boolean> {
|
|
207
|
+
const settings = await AirdropSettings.getSettings(this._networkConnection.provider);
|
|
208
|
+
|
|
209
|
+
if (getCurrentTimestamp() < settings.airdropStartTimestamp) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!this.isWhitelisted(user)) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// iterate for all the market and check if the user has completed the challenge
|
|
218
|
+
return (await Promise.all(this.keepTrackers.map(async (keepTracker) => {
|
|
219
|
+
return keepTracker.isEligible(user);
|
|
220
|
+
}))).includes(true);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export class Airdrop extends BlockchainEntity {
|
|
225
|
+
constructor() {
|
|
226
|
+
super();
|
|
227
|
+
}
|
|
228
|
+
public read(networkConnection: NetworkConnection): AirdropRead {
|
|
229
|
+
return new AirdropRead(networkConnection);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -91,4 +91,9 @@ export const ONE_DAY = 86400;
|
|
|
91
91
|
export const ONE_YEAR = ONE_DAY * 365;
|
|
92
92
|
|
|
93
93
|
export const AurPlyPid = 0;
|
|
94
|
-
export const INF = BN.from(2).pow(256).sub(1)
|
|
94
|
+
export const INF = BN.from(2).pow(256).sub(1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
export const AIRDROP_START_TIMESTAMP = 1649858400;
|
|
98
|
+
export const AIRDROP_END_TIMESTAMP = 1651068000;
|
|
99
|
+
export const AIRDROP_KEEP_THRESHOLD = 7 * 86400;
|
package/src/helpers.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { Provider } from "@ethersproject/abstract-provider";
|
|
1
2
|
import BigNumber from "bignumber.js";
|
|
2
3
|
import { BigNumber as BN, providers, utils } from "ethers";
|
|
3
4
|
import { decimalRecords } from "./decimals";
|
|
4
|
-
import { Address } from "./types";
|
|
5
|
+
import { Address, NetworkConnection } from "./types";
|
|
5
6
|
|
|
6
7
|
export function formatObject(object: Object) {
|
|
7
8
|
return JSON.stringify(object, null, ' ');
|
|
@@ -36,3 +37,55 @@ export function calcLMRewardApr(rewardsValue: BigNumber, totalStakeValue: BigNum
|
|
|
36
37
|
}
|
|
37
38
|
return rewardsValue.multipliedBy(frequencyPerYear).dividedBy(totalStakeValue);
|
|
38
39
|
}
|
|
40
|
+
|
|
41
|
+
export async function getBlockTimestamp(provider: Provider, blockNumber: number): Promise<number> {
|
|
42
|
+
const block = await provider.getBlock(blockNumber);
|
|
43
|
+
return block.timestamp;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the block number of the last block that has a timestamp before the given timestamp
|
|
48
|
+
*
|
|
49
|
+
* Note: For optimized reason, this function is not guaranteed to return the correct block number,
|
|
50
|
+
* it may differ from the actual block number at most 500 blocks.
|
|
51
|
+
*/
|
|
52
|
+
export async function getBlockBeforeTimestamp(provider: Provider, timestamp: number, startBlock?: number): Promise<number> {
|
|
53
|
+
// stop searching if the right - left < blockThreshold
|
|
54
|
+
const blockThreshold = 50 * 10; //50 is the average block in 1 minutes.
|
|
55
|
+
// Using K-search + promise all to speed up the search
|
|
56
|
+
const K = 8;
|
|
57
|
+
|
|
58
|
+
let leftBlock: number = (startBlock || 1);
|
|
59
|
+
let rightBlock = await provider.getBlockNumber();
|
|
60
|
+
|
|
61
|
+
if ((await getBlockTimestamp(provider, rightBlock)) < timestamp) {
|
|
62
|
+
return rightBlock;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
while (rightBlock - leftBlock >= blockThreshold) {
|
|
66
|
+
let size = Math.floor((rightBlock - leftBlock + 1) / K);
|
|
67
|
+
let promises = [];
|
|
68
|
+
|
|
69
|
+
for (let i = 1; i < K; i++) {
|
|
70
|
+
promises.push(getBlockTimestamp(provider, leftBlock + i * size));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let blockTimestamps = await Promise.all(promises);
|
|
74
|
+
let nextLeftBlock = leftBlock;
|
|
75
|
+
|
|
76
|
+
for (let i = 1; i < K; i++) {
|
|
77
|
+
if (blockTimestamps[i - 1] <= timestamp) {
|
|
78
|
+
nextLeftBlock = leftBlock + i * size;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (let i = K - 1; i >= 1; i--) {
|
|
83
|
+
if (blockTimestamps[i - 1] > timestamp) {
|
|
84
|
+
rightBlock = leftBlock + i * size - 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
leftBlock = nextLeftBlock;
|
|
89
|
+
}
|
|
90
|
+
return leftBlock;
|
|
91
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BlockchainEntityRead } from "./BlockchainEntity";
|
|
2
|
+
import { NetworkConnection } from "./types";
|
|
3
|
+
import { AuToken } from "@aurigami/contracts/typechain";
|
|
4
|
+
import AuTokenABI from '@aurigami/contracts/artifacts/contracts/AuToken.sol/AuToken.json';
|
|
5
|
+
import { BigNumber as BN, Contract } from "ethers";
|
|
6
|
+
|
|
7
|
+
type SimpleTransfer = {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
amount: BN;
|
|
11
|
+
blockNumber: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export class TransferEventQuery extends BlockchainEntityRead {
|
|
16
|
+
auToken: AuToken;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
tokenAddr: string,
|
|
20
|
+
networkConnection: NetworkConnection
|
|
21
|
+
) {
|
|
22
|
+
super(networkConnection);
|
|
23
|
+
this.auToken = new Contract(tokenAddr, AuTokenABI.abi, this._networkConnection.provider) as AuToken;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* query ERC20 transfers from/to an address & auto convert them into SimpleTransfer.
|
|
28
|
+
* The result will be sorted by blocknumber.
|
|
29
|
+
*/
|
|
30
|
+
async queryERC20TransfersOf(address: string, fromBlock: number, toBlock: number): Promise<SimpleTransfer[]> {
|
|
31
|
+
const filters = [
|
|
32
|
+
this.auToken.filters.Transfer(address, null),
|
|
33
|
+
this.auToken.filters.Transfer(null, address)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const events = (await Promise.all(filters.map(filter => this.auToken.queryFilter(filter, fromBlock, toBlock)))).flat();
|
|
37
|
+
|
|
38
|
+
let results: SimpleTransfer[] = events.map((event) => {
|
|
39
|
+
return {
|
|
40
|
+
from: event.args![0],
|
|
41
|
+
to: event.args![1],
|
|
42
|
+
amount: event.args![2],
|
|
43
|
+
blockNumber: event.blockNumber,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
results.sort((a, b) => a.blockNumber - b.blockNumber);
|
|
48
|
+
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the balance of an address at a given block number.
|
|
54
|
+
*
|
|
55
|
+
* Note that this will return the balance after the block get mined.
|
|
56
|
+
*/
|
|
57
|
+
async queryERC20BalanceAt(address: string, endBlock: number): Promise<BN> {
|
|
58
|
+
return this.auToken.balanceOf(address, {blockTag: endBlock + 1});
|
|
59
|
+
}
|
|
60
|
+
}
|