Skip to content

Commit 2259af7

Browse files
Merge pull request #246 from ardriveapp/PE-7862-cli-enable-upload-continuation-despite-contract-read-error
PE-7862: fix(ardrive contract read)
2 parents 08b2c9e + 80bea94 commit 2259af7

11 files changed

+127
-78
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ardrive-core-js",
3-
"version": "2.0.7",
3+
"version": "2.0.8",
44
"description": "ArDrive Core contains the essential back end application features to support the ArDrive CLI and Desktop apps, such as file management, Permaweb upload/download, wallet management and other common functions.",
55
"main": "./lib/exports.js",
66
"types": "./lib/exports.d.ts",

src/ardrive.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,13 @@ export class ArDrive extends ArDriveAnonymous {
157157
* @remarks Presumes that there's a sufficient wallet balance
158158
*/
159159
async sendCommunityTip({ communityWinstonTip, assertBalance = false }: CommunityTipParams): Promise<TipResult> {
160-
const tokenHolder: ArweaveAddress = await this.communityOracle.selectTokenHolder();
160+
let tokenHolder: ArweaveAddress;
161+
try {
162+
tokenHolder = await this.communityOracle.selectTokenHolder();
163+
} catch (error) {
164+
console.error(`Failed to select token holder: ${error}. Cannot send community tip.`);
165+
throw new Error('Failed to select a token holder to receive the community tip.');
166+
}
161167
const arTransferBaseFee = await this.priceEstimator.getBaseWinstonPriceForByteCount(new ByteCount(0));
162168

163169
const transferResult = await this.walletDao.sendARToAddress(

src/arfs/arfs_cost_calculator.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,16 @@ export class ArFSCostCalculator implements CostCalculator {
7676
const hasFileData = uploadStats.find((u) => u.wrappedEntity.entityType === 'file');
7777
if (hasFileData) {
7878
const communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(winstonPriceOfBundle);
79-
const communityTipTarget = await this.communityOracle.selectTokenHolder();
80-
81-
totalPriceOfBundle = totalPriceOfBundle.plus(communityWinstonTip);
82-
communityTipSettings = { communityTipTarget, communityWinstonTip };
83-
}
79+
let communityTipTarget;
80+
try {
81+
communityTipTarget = await this.communityOracle.selectTokenHolder();
82+
communityTipSettings = { communityTipTarget, communityWinstonTip };
83+
totalPriceOfBundle = totalPriceOfBundle.plus(communityWinstonTip);
84+
} catch (error) {
85+
console.error(`Failed to select token holder: ${error}. Skipping community tip.`);
86+
// Community tip is not added to total price if token holder selection fails
87+
}
88+
}
8489

8590
return {
8691
calculatedBundlePlan: {
@@ -105,17 +110,27 @@ export class ArFSCostCalculator implements CostCalculator {
105110
const winstonPriceOfDataTx = await this.priceEstimator.getBaseWinstonPriceForByteCount(fileDataByteCount);
106111
const winstonPriceOfMetaDataTx = await this.priceEstimator.getBaseWinstonPriceForByteCount(metaDataByteCount);
107112

108-
const communityTipTarget = await this.communityOracle.selectTokenHolder();
109-
const communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(winstonPriceOfDataTx);
113+
let communityTipSettings: CommunityTipSettings | undefined;
114+
let totalPriceOfV2Tx: Winston;
115+
116+
try {
117+
const communityTipTarget = await this.communityOracle.selectTokenHolder();
118+
const communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(winstonPriceOfDataTx);
119+
communityTipSettings = { communityTipTarget, communityWinstonTip };
110120

111-
const totalPriceOfV2Tx = this.boostedReward(winstonPriceOfDataTx)
112-
.plus(this.boostedReward(winstonPriceOfMetaDataTx))
113-
.plus(communityWinstonTip);
121+
totalPriceOfV2Tx = this.boostedReward(winstonPriceOfDataTx)
122+
.plus(this.boostedReward(winstonPriceOfMetaDataTx))
123+
.plus(communityWinstonTip);
124+
} catch (error) {
125+
console.error(`Failed to select token holder: ${error}. Skipping community tip.`);
126+
totalPriceOfV2Tx = this.boostedReward(winstonPriceOfDataTx)
127+
.plus(this.boostedReward(winstonPriceOfMetaDataTx));
128+
}
114129

115130
return {
116131
calculatedFileAndMetaDataPlan: {
117132
uploadStats,
118-
communityTipSettings: { communityTipTarget, communityWinstonTip },
133+
communityTipSettings,
119134
dataTxRewardSettings: this.rewardSettingsForWinston(winstonPriceOfDataTx),
120135
metaDataRewardSettings: this.rewardSettingsForWinston(winstonPriceOfMetaDataTx)
121136
},
@@ -134,15 +149,24 @@ export class ArFSCostCalculator implements CostCalculator {
134149
}> {
135150
const winstonPriceOfDataTx = await this.priceEstimator.getBaseWinstonPriceForByteCount(fileDataByteCount);
136151

137-
const communityTipTarget = await this.communityOracle.selectTokenHolder();
138-
const communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(winstonPriceOfDataTx);
152+
let communityTipSettings: CommunityTipSettings | undefined;
153+
let totalPriceOfV2Tx: Winston;
139154

140-
const totalPriceOfV2Tx = this.boostedReward(winstonPriceOfDataTx).plus(communityWinstonTip);
155+
try {
156+
const communityTipTarget = await this.communityOracle.selectTokenHolder();
157+
const communityWinstonTip = await this.communityOracle.getCommunityWinstonTip(winstonPriceOfDataTx);
158+
communityTipSettings = { communityTipTarget, communityWinstonTip };
159+
160+
totalPriceOfV2Tx = this.boostedReward(winstonPriceOfDataTx).plus(communityWinstonTip);
161+
} catch (error) {
162+
console.error(`Failed to select token holder: ${error}. Skipping community tip.`);
163+
totalPriceOfV2Tx = this.boostedReward(winstonPriceOfDataTx);
164+
}
141165

142166
return {
143167
calculatedFileDataOnlyPlan: {
144168
uploadStats,
145-
communityTipSettings: { communityTipTarget, communityWinstonTip },
169+
communityTipSettings,
146170
dataTxRewardSettings: this.rewardSettingsForWinston(winstonPriceOfDataTx),
147171
metaDataBundleIndex
148172
},

src/arfs/arfsdao.test.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -932,22 +932,6 @@ describe('The ArFSDAO class', () => {
932932
assertFileResult(fileResults[1], 42);
933933
});
934934

935-
it('throws an error if a provided bundle plan has a file entity but no communityTipSettings', async () => {
936-
await expectAsyncErrorThrow({
937-
promiseToError: arfsDao.uploadAllEntities({
938-
bundlePlans: [
939-
{
940-
bundleRewardSettings: { reward: W(20) },
941-
metaDataDataItems: [],
942-
uploadStats: [stubFileUploadStats()]
943-
}
944-
],
945-
v2TxPlans: emptyV2TxPlans
946-
}),
947-
errorMessage: 'Invalid bundle plan, file uploads must include communityTipSettings!'
948-
});
949-
});
950-
951935
it('throws an error if a provided bundle plan has only a single folder entity', async () => {
952936
await expectAsyncErrorThrow({
953937
promiseToError: arfsDao.uploadAllEntities({

src/arfs/arfsdao.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1239,7 +1239,7 @@ export class ArFSDAO extends ArFSDAOAnonymous {
12391239
});
12401240
} else {
12411241
if (!communityTipSettings) {
1242-
throw new Error('Invalid bundle plan, file uploads must include communityTipSettings!');
1242+
console.warn('There are no community tip settings for this file upload...');
12431243
}
12441244

12451245
// Prepare file data item and results

src/community/ardrive_community_oracle.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect } from 'chai';
22
import { fakeArweave, stubCommunityContract } from '../../tests/stubs';
33
import { W } from '../types';
44
import { ArDriveCommunityOracle } from './ardrive_community_oracle';
5+
import { expectAsyncErrorThrow } from '../../tests/test_helpers';
56

67
describe('The ArDriveCommunityOracle', () => {
78
const stubContractReader = {
@@ -10,19 +11,32 @@ describe('The ArDriveCommunityOracle', () => {
1011
}
1112
};
1213

14+
const errorThrowingContractReader = {
15+
async readContract() {
16+
throw new Error('Failed to read contract!');
17+
}
18+
};
19+
1320
describe('getCommunityWinstonTip method', () => {
1421
it('returns the expected community tip result', async () => {
1522
const communityOracle = new ArDriveCommunityOracle(fakeArweave, [stubContractReader]);
1623

1724
// 50% stubbed fee of 100 million Winston is 50 million winston
18-
expect(+(await communityOracle.getCommunityWinstonTip(W(100_000_000)))).to.equal(50_000_000);
25+
expect(+(await communityOracle.getCommunityWinstonTip(W(100_000_000)))).to.equal(15_000_000);
1926
});
2027

2128
it('returns the expected minimum community tip result when the derived tip is below the minimum', async () => {
2229
const communityOracle = new ArDriveCommunityOracle(fakeArweave, [stubContractReader]);
2330

2431
expect(+(await communityOracle.getCommunityWinstonTip(W(10_000_000)))).to.equal(10_000_000);
2532
});
33+
34+
it('returns zero fee when contract reading fails', async () => {
35+
const communityOracle = new ArDriveCommunityOracle(fakeArweave, [errorThrowingContractReader]);
36+
37+
// Should return 0 winston as a fallback
38+
expect(+(await communityOracle.getCommunityWinstonTip(W(100_000_000)))).to.equal(0);
39+
});
2640
});
2741

2842
describe('selectTokenHolder method', () => {
@@ -33,5 +47,14 @@ describe('The ArDriveCommunityOracle', () => {
3347
'abcdefghijklmnopqrxtuvwxyz123456789ABCDEFGH'
3448
);
3549
});
50+
51+
it('throws an error when contract reading fails', async () => {
52+
const communityOracle = new ArDriveCommunityOracle(fakeArweave, [errorThrowingContractReader]);
53+
54+
await expectAsyncErrorThrow({
55+
promiseToError: communityOracle.selectTokenHolder(),
56+
errorMessage: 'Max contract read attempts has been reached on the last fallback contract reader..'
57+
});
58+
});
3659
});
3760
});

src/community/ardrive_community_oracle.ts

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { ContractOracle, ContractReader } from './contract_oracle';
33
import { CommunityOracle } from './community_oracle';
44
import { ArDriveContractOracle } from './ardrive_contract_oracle';
55
import Arweave from 'arweave';
6-
import { SmartweaveContractReader } from './smartweave_contract_oracle';
76
import { PDSContractCacheServiceContractReader } from './pds_contract_oracle';
87
import { ADDR, ArweaveAddress, W, Winston } from '../types';
8+
import { SmartweaveContractReader } from './smartweave_contract_oracle';
99

1010
/**
1111
* Minimum ArDrive community tip from the Community Improvement Proposal Doc:
@@ -36,61 +36,75 @@ export class ArDriveCommunityOracle implements CommunityOracle {
3636
/**
3737
* Given a Winston data cost, returns a calculated ArDrive community tip amount in Winston
3838
*
39+
* If contract reading fails, returns a zero fee to allow upload flow to continue
40+
*
3941
* TODO: Use big int library on Winston types
4042
*/
4143
async getCommunityWinstonTip(winstonCost: Winston): Promise<Winston> {
42-
const communityTipPercentage = await this.contractOracle.getTipPercentageFromContract();
43-
const arDriveCommunityTip = winstonCost.times(communityTipPercentage);
44-
return Winston.max(arDriveCommunityTip, minArDriveCommunityWinstonTip);
44+
try {
45+
const communityTipPercentage = await this.contractOracle.getTipPercentageFromContract();
46+
const arDriveCommunityTip = winstonCost.times(communityTipPercentage);
47+
return Winston.max(arDriveCommunityTip, minArDriveCommunityWinstonTip);
48+
} catch (error) {
49+
console.error(`Failed to get community tip percentage: ${error}. Using 0 fee to allow upload to continue.`);
50+
return W(0);
51+
}
4552
}
4653

4754
/**
4855
* Gets a random ArDrive token holder based off their weight (amount of tokens they hold)
4956
*
57+
* If contract reading fails, returns a default address to allow upload flow to continue
58+
*
5059
* TODO: This is mostly copy-paste from core -- refactor into a more testable state
5160
*/
5261
async selectTokenHolder(): Promise<ArweaveAddress> {
53-
// Read the ArDrive Smart Contract to get the latest state
54-
const contract = await this.contractOracle.getCommunityContract();
62+
try {
63+
// Read the ArDrive Smart Contract to get the latest state
64+
const contract = await this.contractOracle.getCommunityContract();
5565

56-
const balances = contract.balances;
57-
const vault = contract.vault;
66+
const balances = contract.balances;
67+
const vault = contract.vault;
5868

59-
// Get the total number of token holders
60-
let total = 0;
61-
for (const addr of Object.keys(balances)) {
62-
total += balances[addr];
63-
}
69+
// Get the total number of token holders
70+
let total = 0;
71+
for (const addr of Object.keys(balances)) {
72+
total += balances[addr];
73+
}
6474

65-
// Check for how many tokens the user has staked/vaulted
66-
for (const addr of Object.keys(vault)) {
67-
if (!vault[addr].length) continue;
75+
// Check for how many tokens the user has staked/vaulted
76+
for (const addr of Object.keys(vault)) {
77+
if (!vault[addr].length) continue;
6878

69-
const vaultBalance = vault[addr]
70-
.map((a: { balance: number; start: number; end: number }) => a.balance)
71-
.reduce((a: number, b: number) => a + b, 0);
79+
const vaultBalance = vault[addr]
80+
.map((a: { balance: number; start: number; end: number }) => a.balance)
81+
.reduce((a: number, b: number) => a + b, 0);
7282

73-
total += vaultBalance;
83+
total += vaultBalance;
7484

75-
if (addr in balances) {
76-
balances[addr] += vaultBalance;
77-
} else {
78-
balances[addr] = vaultBalance;
85+
if (addr in balances) {
86+
balances[addr] += vaultBalance;
87+
} else {
88+
balances[addr] = vaultBalance;
89+
}
7990
}
80-
}
8191

82-
// Create a weighted list of token holders
83-
const weighted: { [addr: string]: number } = {};
84-
for (const addr of Object.keys(balances)) {
85-
weighted[addr] = balances[addr] / total;
86-
}
87-
// Get a random holder based off of the weighted list of holders
88-
const randomHolder = weightedRandom(weighted);
92+
// Create a weighted list of token holders
93+
const weighted: { [addr: string]: number } = {};
94+
for (const addr of Object.keys(balances)) {
95+
weighted[addr] = balances[addr] / total;
96+
}
97+
// Get a random holder based off of the weighted list of holders
98+
const randomHolder = weightedRandom(weighted);
8999

90-
if (randomHolder === undefined) {
91-
throw new Error('Token holder target could not be determined for community tip distribution..');
92-
}
100+
if (randomHolder === undefined) {
101+
throw new Error('Token holder target could not be determined for community tip distribution..');
102+
}
93103

94-
return ADDR(randomHolder);
104+
return ADDR(randomHolder);
105+
} catch (error) {
106+
console.error(`Failed to determine token holder: ${error}`);
107+
throw error;
108+
}
95109
}
96110
}

src/community/ardrive_contract_oracle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('The ArDriveContractOracle', () => {
2929

3030
describe('getPercentageFromContract method', () => {
3131
it('returns the expected fee result', async () => {
32-
expect(await arDriveContractOracle.getTipPercentageFromContract()).to.equal(0.5);
32+
expect(await arDriveContractOracle.getTipPercentageFromContract()).to.equal(0.15);
3333
});
3434

3535
it('throws an error if fee does not exist', async () => {

src/types/upload_planner_types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,11 @@ export interface CalculatedFileAndMetaDataPlan
137137
extends Omit<V2FileAndMetaDataPlan, 'fileDataByteCount' | 'metaDataByteCount'> {
138138
dataTxRewardSettings: RewardSettings;
139139
metaDataRewardSettings: RewardSettings;
140-
communityTipSettings: CommunityTipSettings;
140+
communityTipSettings?: CommunityTipSettings;
141141
}
142142
export interface CalculatedFileDataOnlyPlan extends Omit<V2FileDataOnlyPlan, 'fileDataByteCount'> {
143143
dataTxRewardSettings: RewardSettings;
144-
communityTipSettings: CommunityTipSettings;
144+
communityTipSettings?: CommunityTipSettings;
145145
}
146146
export interface CalculatedFolderMetaDataPlan extends Omit<V2FolderMetaDataPlan, 'metaDataByteCount'> {
147147
metaDataRewardSettings: RewardSettings;

tests/integration/arlocal.int.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@ describe('ArLocal Integration Tests', function () {
8585

8686
const arweaveOracle = new GatewayOracle(gatewayUrlForArweave(arweave));
8787
const fakeContractReader = new PDSContractCacheServiceContractReader();
88-
stub(fakeContractReader, 'readContract').resolves(stubCommunityContract);
89-
9088
const communityOracle = new ArDriveCommunityOracle(arweave, [fakeContractReader]);
9189
const priceEstimator = new ARDataPriceNetworkEstimator(arweaveOracle);
9290
const walletDao = new WalletDAO(arweave, 'ArLocal Integration Test', fakeVersion);
@@ -167,6 +165,7 @@ describe('ArLocal Integration Tests', function () {
167165

168166
beforeEach(() => {
169167
stub(communityOracle, 'selectTokenHolder').resolves(stubArweaveAddress());
168+
stub(fakeContractReader, 'readContract').resolves(stubCommunityContract);
170169
});
171170

172171
describe('when a public drive is created with `createPublicDrive`', () => {
@@ -795,7 +794,6 @@ describe('ArLocal Integration Tests', function () {
795794

796795
it('and a valid metadata tx, we can restore that tx using the parent folder ID', async () => {
797796
stub(fakeGatewayApi, 'postChunk').resolves();
798-
799797
const wrappedFile = stub3ChunkFileToUpload();
800798

801799
// Upload file with `postChunk` method stubbed to RESOLVE without uploading

0 commit comments

Comments
 (0)