@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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.2.5",
2
+ "version": "1.3.0",
3
3
  "license": "MIT",
4
4
  "main": "dist/index.js",
5
5
  "typings": "dist/index.d.ts",
@@ -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
@@ -15,3 +15,5 @@ export * from './priceFetcher';
15
15
  export * from './token';
16
16
  export * from './tokenAmount';
17
17
  export * from './types';
18
+ export * from './airdrop-lottery';
19
+ export * from './transfer-event-query';
@@ -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
+ }