Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(datastore): enable update and delete queries
- Loading branch information
1 parent
e844ebe
commit 972029e
Showing
45 changed files
with
783 additions
and
570 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { Command } from 'commander'; | ||
import ArgonUtils from '@ulixee/sidechain/lib/ArgonUtils'; | ||
import DatastoreApiClient from '../lib/DatastoreApiClient'; | ||
import CreditsStore from '../lib/CreditsStore'; | ||
|
||
export default function creditsCli(): Command { | ||
const cli = new Command('credits'); | ||
cli | ||
.command('create') | ||
.description('Create Credits for a User to try out your Datastore.') | ||
.argument('<url>', 'The url to the Datastore.') | ||
.requiredOption( | ||
'-m, --amount <value>', | ||
'The value of this Credit. Amount can postfix "c" for centagons (eg, 50c) or "m" for microgons (5000000m).', | ||
/\d+[mc]?/, | ||
) | ||
.requiredOption('-i, --identity <idBech32String>', 'You administration identity keypair.') | ||
.action(async (url, { identity, amount }) => { | ||
const parsedUrl = new URL(url); | ||
const client = new DatastoreApiClient(parsedUrl.origin); | ||
const microgons = ArgonUtils.parseUnits(amount, 'microgons'); | ||
const result = await client.createCredits(parsedUrl.pathname, microgons, identity); | ||
|
||
// TODO: output a url to send the user to | ||
console.log(`Credit created!`, { credit: result }); | ||
}); | ||
|
||
cli | ||
.command('install') | ||
.description('Save to a local wallet.') | ||
.argument('<url>', 'The url of the Credit.') | ||
.argument('<secret>', 'The Credit secret.') | ||
.action(async (url, secret) => { | ||
const parsedUrl = new URL(url); | ||
const client = new DatastoreApiClient(parsedUrl.origin); | ||
const [datastoreVersion, id] = parsedUrl.pathname.split('/credit/'); | ||
const { balance } = await client.getCreditsBalance(datastoreVersion, id); | ||
await CreditsStore.store(parsedUrl.origin, datastoreVersion.replace(/\//g, ''), { | ||
id, | ||
secret, | ||
remainingCredits: balance, | ||
}); | ||
}); | ||
|
||
cli.command('get').description('Get the current balance and holds.'); | ||
return cli; | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { readFileAsJson, safeOverwriteFile } from '@ulixee/commons/lib/fileUtils'; | ||
import { getCacheDirectory } from '@ulixee/commons/lib/dirUtils'; | ||
import { IPayment } from '@ulixee/specification'; | ||
|
||
export default class CreditsStore { | ||
public static storePath = `${getCacheDirectory()}/ulixee/credits.json`; | ||
|
||
private static creditsByDatastore: Promise<ICreditsStore>; | ||
|
||
public static async store( | ||
minerHost: string, | ||
datastoreVersionHash: string, | ||
credits: { id: string; secret: string; remainingCredits: number }, | ||
): Promise<void> { | ||
const allCredits = await this.load(); | ||
allCredits[`${minerHost}/${datastoreVersionHash}`] ??= {}; | ||
allCredits[`${minerHost}/${datastoreVersionHash}`][credits.id] = credits; | ||
await this.writeToDisk(allCredits); | ||
} | ||
|
||
public static async getPayment( | ||
minerHost: string, | ||
datastoreVersionHash: string, | ||
microgons: number, | ||
): Promise< | ||
(IPayment & { onFinalized(result: { microgons: number; bytes: number }): void }) | undefined | ||
> { | ||
const credits = await this.load(); | ||
const datastoreCredits = credits[`${minerHost}/${datastoreVersionHash}`]; | ||
if (!datastoreCredits) return; | ||
|
||
for (const [creditId, credit] of Object.entries(datastoreCredits)) { | ||
if (credit.remainingCredits >= microgons) { | ||
credit.remainingCredits -= microgons; | ||
return { | ||
credits: { id: creditId, secret: credit.secret }, | ||
onFinalized: this.finalizePayment.bind(this, credit, microgons), | ||
}; | ||
} | ||
} | ||
} | ||
|
||
protected static finalizePayment( | ||
originalMicrogons: number, | ||
credits: ICreditsStore[0][0], | ||
result: { microgons: number; bytes: number }, | ||
): void { | ||
if (!result) return; | ||
const fundsToReturn = originalMicrogons - result.microgons; | ||
if (fundsToReturn && Number.isInteger(fundsToReturn)) { | ||
credits.remainingCredits += fundsToReturn; | ||
} | ||
} | ||
|
||
private static async load(): Promise<ICreditsStore> { | ||
this.creditsByDatastore ??= readFileAsJson<ICreditsStore>(this.storePath).catch(() => ({})); | ||
return await this.creditsByDatastore; | ||
} | ||
|
||
private static writeToDisk(data: any): Promise<void> { | ||
return safeOverwriteFile(this.storePath, data); | ||
} | ||
} | ||
interface ICreditsStore { | ||
[versionHost: string]: { [creditsId: string]: { secret: string; remainingCredits: number } }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { buffer, ExtractSchemaType, number, string } from '@ulixee/schema'; | ||
import { sha3 } from '@ulixee/commons/lib/hashUtils'; | ||
import { concatAsBuffer } from '@ulixee/commons/lib/bufferUtils'; | ||
import { customAlphabet, nanoid } from 'nanoid'; | ||
import Table from './Table'; | ||
|
||
const postgresFriendlyNanoid = customAlphabet( | ||
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz', | ||
); | ||
|
||
export default class CreditsTable extends Table<typeof CreditsSchema> { | ||
public static tableName = 'ulx_credits'; | ||
constructor() { | ||
super({ | ||
name: CreditsTable.tableName, | ||
isPublic: false, | ||
description: 'Private table tracking Credits issued for the containing Datastore.', | ||
schema: CreditsSchema, | ||
pricePerQuery: 0, | ||
}); | ||
} | ||
|
||
async create( | ||
microgons: number, | ||
secret?: string, | ||
): Promise<{ id: string; secret: string; remainingCredits: number }> { | ||
const salt = nanoid(16); | ||
const id = `cred${postgresFriendlyNanoid(8)}`; | ||
secret ??= postgresFriendlyNanoid(12); | ||
const secretHash = sha3(concatAsBuffer(id, salt, secret)); | ||
await this.query( | ||
'INSERT INTO self (id, salt, secretHash, issuedCredits, heldCredits, remainingCredits) VALUES ($1, $2, $3, $4, 0, $4)', | ||
[id, salt, secretHash, microgons], | ||
); | ||
return { id, secret, remainingCredits: microgons }; | ||
} | ||
|
||
async get(id: string): Promise<Omit<ICredit, 'salt' | 'secretHash'>> { | ||
const [credit] = await this.query( | ||
'SELECT id, issuedCredits, remainingCredits, heldCredits FROM self WHERE id = $1', | ||
[id], | ||
); | ||
return credit; | ||
} | ||
|
||
async hold(id: string, secret: string, holdAmount: number): Promise<number> { | ||
const [credit] = await this.query('SELECT * FROM self WHERE id=$1', [id]); | ||
if (!credit) throw new Error('This is an invalid Credit.'); | ||
|
||
const hash = sha3(concatAsBuffer(credit.id, credit.salt, secret)); | ||
if (!hash.equals(credit.secretHash)) throw new Error('This is an invalid Credit secret.'); | ||
|
||
const result = (await this.query( | ||
'UPDATE self SET heldCredits = heldCredits + $2 ' + | ||
'WHERE id = $1 AND (remainingCredits - heldCredits - $2) >= 0 ' + | ||
'RETURNING remainingCredits', | ||
[id, holdAmount], | ||
)) as any; | ||
|
||
if (result === undefined) throw new Error('Could not hold funds from the given Credits.'); | ||
return result.remainingCredits; | ||
} | ||
|
||
async finalize(id: string, holdAmount: number, finalAmount: number): Promise<number> { | ||
const result = (await this.query( | ||
'UPDATE self SET heldCredits = heldCredits + $2, remainingCredits = remainingCredits - $3 ' + | ||
'WHERE id = $1 ' + | ||
'RETURNING remainingCredits', | ||
[id, holdAmount, finalAmount], | ||
)) as any; | ||
if (result === undefined) throw new Error('Could not finalize payment for the given Credits.'); | ||
return result.remainingCredits; | ||
} | ||
} | ||
|
||
export const CreditsSchema = { | ||
id: string({ regexp: /^cred[A-Za-z0-9_-]{8}$/ }), | ||
salt: string({ length: 16 }), | ||
secretHash: buffer(), | ||
issuedCredits: number(), | ||
heldCredits: number(), | ||
remainingCredits: number(), | ||
}; | ||
type ICredit = ExtractSchemaType<typeof CreditsSchema>; |
Oops, something went wrong.