Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add push consent & display fees #21

Merged
merged 8 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-clouds-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@protocol.land/git-remote-helper': minor
---

feat: Add push consent with threshold configuration & display fees
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,34 @@ To enable `git push` or gain write access to repositories, you'll need an Arweav
git config --global --add protocol.land.keyfile ~/private_folder/jwk_keyfile.json
```

> **Note:** This globally adds the keyfile path for all repositories. If you prefer to use them selectively per repository, omit the `--global` modifier in the `git config` command.
> [!Note]
> This globally adds the keyfile path for all repositories. If you prefer to use them selectively per repository, omit the `--global` modifier in the `git config` command.

## Setup Threshold Cost for Push consent

> [!Note]
> This functionality is compatible with UNIX-based operating systems such as Linux, macOS etc. For Windows users, leveraging the Windows Subsystem for Linux (WSL) is recommended.

To effectively manage push consent based on the cost of pushing changes, you can configure a Threshold Cost. Use the `git config` command to set this threshold value:

```bash
git config --global --add protocol.land.thresholdCost 0.0003
```

This command sets the global threshold cost for push consent to `0.0003 AR`. When the estimated push cost exceeds this threshold, users will be prompted to consent to the fee before proceeding with the push.

> [!Note]
> This threshold is set globally for all repositories. If you wish to apply different thresholds for specific repositories, use the command without the `--global` modifier within the repository's directory.

### Understanding Push Consent Logic

Here's how it decides when to ask for your consent before uploading:

- **No Set Threshold**: Without the threshold set, you'll only be asked for consent if the upload size exceeds the free subsidy size (For example: Turbo bundler used here allows upto 500KB uploads for free).
- **Over the Threshold**: If the upload cost is more than the threshold, consent is requested only if the upload size is larger than what's freely allowed.
- **Under the Threshold**: For costs below the threshold, consent isn't needed, and uploads proceed automatically.

Adjusting the threshold cost allows users and organizations to maintain control over their expenditure on network fees, ensuring transparency and consent for every push operation that incurs a cost above the specified threshold.

## Usage

Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"scripts": {
"lint": "tsc",
"build": "tsup src/index.ts --format esm --dts",
"build": "tsup src/index.ts --format esm",
"release": "pnpm run build && changeset publish"
},
"keywords": [
Expand All @@ -26,9 +26,7 @@
},
"files": [
"dist/",
"src/",
"package.json",
"tsconfig.json",
"LICENSE",
"README.md"
],
Expand All @@ -50,7 +48,8 @@
"arweave": "^1.14.4",
"jszip": "^3.10.1",
"node-machine-id": "^1.1.12",
"redstone-api": "^0.4.11",
"uuid": "^9.0.1",
"warp-contracts": "^1.4.19"
"warp-contracts": "^1.4.25"
}
}
70 changes: 69 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

134 changes: 115 additions & 19 deletions src/lib/arweaveHelper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Arweave from 'arweave';
import { ArweaveSigner, createData } from 'arbundles';
import { getWallet, log } from './common';
import readline from 'node:readline';
import fs, { promises as fsPromises } from 'fs';
import { getThresholdCost, getWallet, initArweave, log } from './common';
import type { Tag } from '../types';
import { withAsync } from './withAsync';
import type { JsonWebKey } from 'crypto';
Expand All @@ -19,30 +20,125 @@ export function getActivePublicKey() {
return wallet.n;
}

export async function uploadRepo(zipBuffer: Buffer, tags: Tag[]) {
async function checkAccessToTty() {
try {
// upload compressed repo using turbo
const turboTxId = await turboUpload(zipBuffer, tags);
log(`Posted Tx to Turbo: ${turboTxId}`);
return turboTxId;
} catch (error) {
// dismiss error and try with arweave
log('Turbo failed, trying with Arweave...');
// let Arweave throw if it encounters errors
const arweaveTxId = await arweaveUpload(zipBuffer, tags);
log(`Posted Tx to Arweave: ${arweaveTxId}`);
return arweaveTxId;
await fsPromises.access(
'/dev/tty',
fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK
);
return true;
} catch (err) {
return false;
}
}

function initArweave() {
return Arweave.init({
host: 'arweave.net',
port: 443,
protocol: 'https',
function createTtyReadlineInterface() {
const ttyReadStream = fs.createReadStream('/dev/tty');
const ttyWriteStream = fs.createWriteStream('/dev/tty');

const rl = readline.createInterface({
input: ttyReadStream,
output: ttyWriteStream,
});

return {
rl,
ttyReadStream,
ttyWriteStream,
};
}

function askQuestionThroughTty(question: string): Promise<string> {
return new Promise((resolve, reject) => {
const { rl, ttyReadStream, ttyWriteStream } =
createTtyReadlineInterface();

rl.question(question, (answer: string) => {
rl.close();
ttyReadStream.destroy();
ttyWriteStream.end();
ttyWriteStream.on('finish', () => {
resolve(answer.trim().toLowerCase());
});
});

rl.on('error', (err) => reject(err));
ttyReadStream.on('error', (err) => reject(err));
ttyWriteStream.on('error', (err) => reject(err));
});
}

const shouldPushChanges = async (
uploadSize: number,
uploadCost: number,
subsidySize: number
) => {
let hasAccessToTty = await checkAccessToTty();

// If no access to TTY, proceed with push by default.
if (!hasAccessToTty) return true;

const thresholdCost = getThresholdCost();

let showPushConsent;

if (thresholdCost === null) {
// No threshold: Show consent only if above strategy's subsidy.
showPushConsent = uploadSize > subsidySize;
} else if (uploadCost > thresholdCost) {
// Above Threshold: Show consent only if above strategy's subsidy.
showPushConsent = uploadSize > subsidySize;
} else {
// Below Threshold: Don't show consent.
showPushConsent = false;
}

// If no consent needed, proceed with push.
if (!showPushConsent) return true;

// Ask for user consent through TTY.
try {
const answer = await askQuestionThroughTty(' [PL] Push? (y/n): ');
return answer === 'yes' || answer === 'y';
} catch (err) {
return true;
}
};

export async function uploadRepo(
zipBuffer: Buffer,
tags: Tag[],
uploadSize: number,
uploadCost: number
) {
// 500KB subsidySize for TurboUpload and 0 subsidySize for ArweaveUpload
const subsidySize = Math.max(500 * 1024, 0);
const pushChanges = await shouldPushChanges(
uploadSize,
uploadCost,
subsidySize
);

async function attemptUpload(
uploaderName: string,
uploader: (buffer: Buffer, tags: Tag[]) => Promise<string>
) {
if (pushChanges) {
const txId = await uploader(zipBuffer, tags);
log(`Posted Tx to ${uploaderName}: ${txId}`);
return { txId, pushCancelled: false };
}
return { txid: '', pushCancelled: true };
}

try {
return await attemptUpload('Turbo', turboUpload);
} catch (error) {
log('Turbo failed, trying with Arweave...');
return await attemptUpload('Arweave', arweaveUpload);
}
}

export async function arweaveDownload(txId: string) {
const { response, error } = await withAsync(() =>
fetch(`https://arweave.net/${txId}`)
Expand Down
Loading
Loading