1
0
mirror of https://github.com/oceanprotocol-archive/squid-js.git synced 2024-02-02 15:31:51 +01:00

initial implementation of templates and conditions

This commit is contained in:
Pedro Gutiérrez 2019-02-28 21:28:57 +01:00 committed by Pedro Gutiérrez
parent 19287469bc
commit 5ec624b2a5
22 changed files with 438 additions and 172 deletions

View File

@ -0,0 +1,109 @@
import { assert } from 'chai'
import { config } from "../config"
import { Ocean, templates, conditions, generateId, Keeper } from '../../src' // @oceanprotocol/squid
const { LockRewardCondition, EscrowReward, AccessSecretStoreCondition } = conditions
const { EscrowAccessSecretStoreTemplate } = templates
describe("Register Escrow Access Secret Store Template", () => {
let ocean: Ocean
let keeper: Keeper
const agreementId = `0x${generateId()}`
const escrowAmount = 12
const did = `0x${"a".repeat(64)}`
const url = 'https://example.com/did/ocean/test-attr-example.txt'
const checksum = "b".repeat(32)
let templateManagerOwner: string
let sender: string
let receiver: string
let accessSecretStoreCondition: conditions.AccessSecretStoreCondition
let lockRewardCondition: conditions.LockRewardCondition
let escrowReward: conditions.EscrowReward
let template: templates.EscrowAccessSecretStoreTemplate
let conditionIdAccess: string
let conditionIdLock: string
let conditionIdEscrow: string
before(async () => {
ocean = await Ocean.getInstance(config)
keeper = await Keeper.getInstance()
template = await EscrowAccessSecretStoreTemplate.getInstance()
// Accounts
templateManagerOwner = (await ocean.accounts.list())[0].getId()
sender = (await ocean.accounts.list())[1].getId()
receiver = (await ocean.accounts.list())[2].getId()
// Conditions
accessSecretStoreCondition = await AccessSecretStoreCondition.getInstance()
lockRewardCondition = await LockRewardCondition.getInstance()
escrowReward = await EscrowReward.getInstance()
})
it("should propose the template", async () => {
await keeper.templateStoreManager.proposeTemplate(template.getAddress(), receiver, true)
// TODO: Use a event to detect template mined
await new Promise(_ => setTimeout(_, 6 * 1000))
})
it("should approve the template", async () => {
await keeper.templateStoreManager.approveTemplate(template.getAddress(), templateManagerOwner, true)
// TODO: Use a event to detect template mined
await new Promise(_ => setTimeout(_, 6 * 1000))
})
it("should generate the condition IDs", async () => {
conditionIdAccess = await accessSecretStoreCondition.generateId(agreementId, await accessSecretStoreCondition.hashValues(did, receiver))
conditionIdLock = await lockRewardCondition.generateIdHash(agreementId, await escrowReward.getAddress(), escrowAmount)
conditionIdEscrow = await escrowReward.generateId(agreementId, await escrowReward.hashValues(escrowAmount, receiver, sender, conditionIdLock, conditionIdAccess))
})
it("should have conditions types", async () => {
const conditionTypes = await template.getConditionTypes()
assert.equal(conditionTypes.length, 3, "Expected 3 conditions.")
assert.deepEqual(
[...conditionTypes].sort(),
[accessSecretStoreCondition.getAddress(), escrowReward.getAddress(), lockRewardCondition.getAddress()].sort(),
"The conditions doesn't match",
)
})
it("should have condition instances asociated", async () => {
const conditions = await template.getConditions()
assert.equal(conditions.length, 3, "Expected 3 conditions.")
const conditionClasses = [AccessSecretStoreCondition, EscrowReward, LockRewardCondition]
conditionClasses
.forEach(conditionClass => {
if (!conditions.find(condition => condition instanceof conditionClass)) {
throw `${conditionClass.name} is not part of the conditions.`;
}
})
})
it("should create a new agreement", async () => {
await keeper.didRegistry.registerAttribute(did.replace("0x", ""), checksum, url, sender)
const agreement = await template.createAgreement(
agreementId,
did,
[conditionIdAccess, conditionIdLock, conditionIdEscrow],
[0, 0, 0],
[0, 0, 0],
receiver,
)
assert.isTrue(agreement.status)
})
})

View File

@ -1,28 +0,0 @@
import { assert } from 'chai'
import { config } from "../config"
import { Ocean, Account, ServiceAgreementTemplate, Templates } from '../../src' // @oceanprotocol/squid
describe("Register Service Agreement Templates", () => {
let ocean: Ocean
let templateOwner: Account
before(async () => {
ocean = await Ocean.getInstance(config)
// Accounts
templateOwner = (await ocean.accounts.list())[0]
})
it("should regiester a template", async () => {
const serviceAgreementTemplate = new ServiceAgreementTemplate(new Templates.Access())
const serviceAgreementRegistered = await serviceAgreementTemplate.register(templateOwner.getId())
try {
// It can fail because is already created
assert.isTrue(serviceAgreementRegistered, "Service agreement template not registered correctly")
} catch (e) { }
})
})

View File

@ -59,7 +59,7 @@
},
"homepage": "https://github.com/oceanprotocol/squid-js#readme",
"dependencies": {
"@oceanprotocol/keeper-contracts": "^0.6.12",
"@oceanprotocol/keeper-contracts": "^0.7.0",
"@oceanprotocol/secret-store-client": "~0.0.14",
"@types/node-fetch": "^2.1.4",
"bignumber.js": "^8.0.1",

View File

@ -1,9 +1,9 @@
import AccessConditions from "./contracts/conditions/AccessConditions"
import PaymentConditions from "./contracts/conditions/PaymentConditions"
import DIDRegistry from "./contracts/DIDRegistry"
import Dispenser from "./contracts/Dispenser"
import ServiceExecutionAgreement from "./contracts/ServiceExecutionAgreement"
import OceanToken from "./contracts/Token"
import { Condition, LockRewardCondition, EscrowReward, AccessSecretStoreCondition } from "./contracts/conditions"
import { EscrowAccessSecretStoreTemplate } from "./contracts/templates"
import { TemplateStoreManager } from "./contracts/managers"
import Web3Provider from "./Web3Provider"
@ -21,17 +21,30 @@ export default class Keeper {
* @return {Promise<Keeper>}
*/
public static async getInstance(): Promise<Keeper> {
if (Keeper.instance === null) {
Keeper.instance = new Keeper()
// Main contracts
Keeper.instance.dispenser = await Dispenser.getInstance()
Keeper.instance.token = await OceanToken.getInstance()
Keeper.instance.serviceAgreement = await ServiceExecutionAgreement.getInstance()
Keeper.instance.accessConditions = await AccessConditions.getInstance()
Keeper.instance.paymentConditions = await PaymentConditions.getInstance()
Keeper.instance.didRegistry = await DIDRegistry.getInstance()
// Managers
Keeper.instance.templateStoreManager = await TemplateStoreManager.getInstance()
// Conditions
Keeper.instance.conditions = {
lockRewardCondition: await LockRewardCondition.getInstance(),
escrowReward: await EscrowReward.getInstance(),
accessSecretStoreCondition: await AccessSecretStoreCondition.getInstance(),
}
// Conditions
Keeper.instance.templates = {
escrowAccessSecretStoreTemplate: await EscrowAccessSecretStoreTemplate.getInstance(),
}
}
return Keeper.instance
}
@ -53,30 +66,44 @@ export default class Keeper {
*/
public dispenser: Dispenser
/**
* Service agreement smart contract instance.
* @type {ServiceExecutionAgreement}
*/
public serviceAgreement: ServiceExecutionAgreement
/**
* Access conditions smart contract instance.
* @type {AccessConditions}
*/
public accessConditions: AccessConditions
/**
* Payment conditions smart contract instance.
* @type {PaymentConditions}
*/
public paymentConditions: PaymentConditions
/**
* DID registry smart contract instance.
* @type {DIDRegistry}
*/
public didRegistry: DIDRegistry
/**
* Template store manager smart contract instance.
* @type {TemplateStoreManager}
*/
public templateStoreManager: TemplateStoreManager
/**
* Conditions instances.
*/
public conditions: {
lockRewardCondition: LockRewardCondition,
escrowReward: EscrowReward,
accessSecretStoreCondition: AccessSecretStoreCondition,
}
/**
* Templates instances.
*/
public templates: {
escrowAccessSecretStoreTemplate: EscrowAccessSecretStoreTemplate,
}
/**
* Returns a condition by address.
* @param {string} address Address of deployed condition.
* @return {Condition} Condition instance.
*/
public getConditionByAddress(address: string): Condition {
return Object.values(this.conditions)
.find(condition => condition.getAddress() === address)
}
/**
* Returns the network by name.
* @return {Promise<string>} Network name.

View File

@ -3,6 +3,7 @@ import Contract from "web3-eth-contract"
import {Receipt} from "web3-utils"
import Logger from "../../utils/Logger"
import ContractHandler from "../ContractHandler"
import Web3Provider from "../Web3Provider"
export default abstract class ContractBase {
@ -22,7 +23,7 @@ export default abstract class ContractBase {
return this.contract.getPastEvents(eventName, options)
}
public getAddress() {
public getAddress(): string {
return this.contract.options.address
}
@ -40,6 +41,13 @@ export default abstract class ContractBase {
this.contract = await ContractHandler.get(this.contractName)
}
protected async sendFrom(name: string, args: any[], from?: string): Promise<Receipt> {
if (!from) {
from = (await Web3Provider.getWeb3().eth.getAccounts())[0]
}
return this.send(name, from, args)
}
protected async send(name: string, from: string, args: any[]): Promise<Receipt> {
if (!this.contract.methods[name]) {
throw new Error(`Method "${name}" is not part of contract "${this.contractName}"`)
@ -73,7 +81,7 @@ export default abstract class ContractBase {
}
}
protected async call(name: string, args: any[], from?: string): Promise<any> {
protected async call<T extends any>(name: string, args: any[], from?: string): Promise<T> {
if (!this.contract.methods[name]) {
throw new Error(`Method ${name} is not part of contract ${this.contractName}`)
}

View File

@ -19,8 +19,6 @@ export default class DIDRegistry extends ContractBase {
}
public async getUpdateAt(did: string): Promise<number> {
const blockNum = await this.call("getUpdateAt", ["0x" + did])
return parseInt(blockNum, 10)
return +await this.call("getUpdateAt", ["0x" + did])
}
}

View File

@ -1,58 +0,0 @@
import {Receipt} from "web3-utils"
import MethodReflection from "../../models/MethodReflection"
import DID from "../../ocean/DID"
import ContractBase from "./ContractBase"
export default class ServiceExecutionAgreement extends ContractBase {
public static async getInstance(): Promise<ServiceExecutionAgreement> {
const serviceAgreement: ServiceExecutionAgreement = new ServiceExecutionAgreement("ServiceExecutionAgreement")
await serviceAgreement.init()
return serviceAgreement
}
public async setupTemplate(
templateId: string,
methodReflections: MethodReflection[],
dependencyMatrix: number[],
fulfillmentIndices: number[],
fulfillmentOperator: number,
ownerAddress: string,
): Promise<Receipt> {
return this.send("setupTemplate", ownerAddress, [
templateId, methodReflections.map((r) => r.address),
methodReflections.map((r) => r.signature), dependencyMatrix, fulfillmentIndices,
fulfillmentOperator,
])
}
// todo get service agreement consumer
public async getTemplateStatus(templateId: string) {
return this.call("getTemplateStatus", [templateId])
}
public async getTemplateOwner(templateId: string) {
return this.call("getTemplateOwner", [templateId])
}
public async initializeAgreement(
serviceAgreementTemplateId: string,
serviceAgreementSignatureHash: string,
consumerAddress: string,
valueHashes: string[],
timeoutValues: number[],
serviceAgreementId: string,
did: DID,
publisherAddress: string,
): Promise<Receipt> {
return this.send("initializeAgreement", publisherAddress, [
serviceAgreementTemplateId, serviceAgreementSignatureHash, consumerAddress, valueHashes,
timeoutValues, "0x" + serviceAgreementId, "0x" + did.getId(),
])
}
}

View File

@ -1,20 +0,0 @@
import {Receipt} from "web3-utils"
import ContractBase from "../ContractBase"
export default class AccessConditions extends ContractBase {
public static async getInstance(): Promise<AccessConditions> {
const accessConditions: AccessConditions = new AccessConditions("AccessConditions")
await accessConditions.init()
return accessConditions
}
// todo add check permissions proxy
public async grantAccess(serviceAgreementId: any, documentKeyId: any, publisherAddress: string)
: Promise<Receipt> {
return this.send("grantAccess", publisherAddress, [
serviceAgreementId, "0x" + documentKeyId,
])
}
}

View File

@ -0,0 +1,16 @@
import { Condition } from "./Condition.abstract"
export class AccessSecretStoreCondition extends Condition {
public static async getInstance(): Promise<AccessSecretStoreCondition> {
return Condition.getInstance("AccessSecretStoreCondition", AccessSecretStoreCondition)
}
hashValues(did: string, grantee: string) {
return super.hashValues(did, grantee)
}
fulfill(agreementId: string, did: string, grantee: string, from?: string) {
return super.fulfill(agreementId, [did, grantee], from)
}
}

View File

@ -0,0 +1,43 @@
import ContractBase from "../ContractBase"
export enum ConditionState {
Uninitialized = 0,
Unfulfilled = 1,
Fulfilled = 2,
Aborted = 3,
}
export abstract class Condition extends ContractBase {
protected constructor(contractName: string) {
super(contractName)
}
public static async getInstance(conditionName: string, conditionsClass: any): Promise<Condition> {
const condition: Condition = new (conditionsClass as any)(conditionName)
await condition.init()
return condition
}
hashValues(...args: any[]): Promise<string> {
return this.call("hashValues", args)
}
fulfill(agreementId: string, ...args: any[]): Promise<ConditionState>
fulfill(agreementId: string, args: any[], from?: string): Promise<ConditionState> {
return this.sendFrom("fulfill", args, from)
}
async generateIdHash(agreementId: string, ...values: any[]): Promise<string> {
return this.generateId(agreementId, await this.hashValues(...values))
}
generateId(agreementId: string, valueHash: string): Promise<string> {
return this.call("generateId", [agreementId, valueHash])
}
abortByTimeOut(agreementId: string, from?: string): Promise<ConditionState> {
return this.sendFrom("requestTokens", [agreementId], from)
}
}

View File

@ -0,0 +1,24 @@
import { Condition } from "./Condition.abstract"
export class EscrowReward extends Condition {
public static async getInstance(): Promise<EscrowReward> {
return Condition.getInstance("EscrowReward", EscrowReward)
}
hashValues(amount: number, receiver: string, sender: string, lockCondition: string, releaseCondition: string) {
return super.hashValues(amount, receiver, sender, lockCondition, releaseCondition)
}
fulfill(
agreementId: string,
amount: number,
receiver: string,
sender: string,
lockCondition: string,
releaseCondition: string,
from?: string,
) {
return super.fulfill(agreementId, [amount, receiver, sender, lockCondition, releaseCondition], from)
}
}

View File

@ -0,0 +1,16 @@
import { Condition } from "./Condition.abstract"
export class LockRewardCondition extends Condition {
public static async getInstance(): Promise<LockRewardCondition> {
return Condition.getInstance("LockRewardCondition", LockRewardCondition)
}
hashValues(rewardAddress: string, amount: number) {
return super.hashValues(rewardAddress, amount)
}
fulfill(agreementId: string, rewardAddress: string, amount: number, from?: string) {
return super.fulfill(agreementId, [rewardAddress, amount], from)
}
}

View File

@ -1,18 +0,0 @@
import {Receipt} from "web3-utils"
import ContractBase from "../ContractBase"
export default class PaymentConditions extends ContractBase {
public static async getInstance(): Promise<PaymentConditions> {
const paymentConditions: PaymentConditions = new PaymentConditions("PaymentConditions")
await paymentConditions.init()
return paymentConditions
}
public async lockPayment(serviceAgreementId: string, assetId: string, price: number, consumerAddress: string)
: Promise<Receipt> {
return this.send("lockPayment", consumerAddress, [
serviceAgreementId, "0x" + assetId, price,
])
}
}

View File

@ -0,0 +1,4 @@
export * from './Condition.abstract'
export { AccessSecretStoreCondition } from './AccessSecretStoreCondition'
export { EscrowReward } from './EscrowReward'
export { LockRewardCondition } from './LockRewardCondition'

View File

@ -0,0 +1,62 @@
import Logger from "../../../utils/Logger"
import ContractBase from "../ContractBase"
export enum TemplateState {
Uninitialized = 0,
Proposed = 1,
Approved = 2,
Revoked = 3,
}
export interface TemplateMetadata {
state: TemplateState,
owner: string,
lastUpdatedBy: string,
blockNumberUpdated: number
}
export class TemplateStoreManager extends ContractBase {
public static async getInstance(): Promise<TemplateStoreManager> {
const templateStoreManeger: TemplateStoreManager = new TemplateStoreManager("TemplateStoreManager")
await templateStoreManeger.init()
return templateStoreManeger
}
getOwner(): Promise<string> {
return this.call("owner", [])
}
public async proposeTemplate(address: string, from?: string, ignoreExists?: boolean) {
const template = await this.getTemplate(address)
if (template.blockNumberUpdated !== 0) {
Logger.warn(`Template "${address}" already exist.`)
if (!ignoreExists) {
throw new Error("Template already exist.")
}
} else {
return this.sendFrom("proposeTemplate", [address], from)
}
}
public async approveTemplate(address: string, from?: string, ignoreApproved?: boolean) {
const template = await this.getTemplate(address)
if (template.state !== TemplateState.Proposed) {
Logger.warn(`Template "${address}" is not in "proposed" state.`)
if (!ignoreApproved) {
throw new Error(`Template not in "proposed" state.`)
}
} else {
return this.sendFrom("approveTemplate", [address], from)
}
}
public revokeTemplate(address: string, from?: string) {
return this.sendFrom("revokeTemplate", [address], from)
}
public async getTemplate(address: string) {
const {state, owner, lastUpdatedBy, blockNumberUpdated} = await this.call("getTemplate", [address])
return {state: +state, owner, lastUpdatedBy, blockNumberUpdated: +blockNumberUpdated} as TemplateMetadata
}
}

View File

@ -0,0 +1 @@
export * from './TemplateStoreManager'

View File

@ -0,0 +1,52 @@
import ContractBase from "../ContractBase"
import { Condition } from "../conditions/Condition.abstract"
import Keeper from "../../Keeper"
export abstract class AgreementTemplate extends ContractBase {
protected constructor(contractName: string) {
super(contractName)
}
public static async getInstance(conditionName: string, templateClass: any): Promise<AgreementTemplate> {
const condition: AgreementTemplate = new (templateClass as any)(conditionName)
await condition.init()
return condition
}
// tslint:disable-next-line
public createAgreement(agreementId: string, did: string, conditionIds: string[], timeLocks: number[], timeOuts: number[], ...args: any[])
public createAgreement(
agreementId: string,
did: string,
conditionIds: string[],
timeLocks: number[],
timeOuts: number[],
extraArgs: any[],
from?: string,
) {
return this.sendFrom(
"createAgreement",
[
agreementId,
did,
conditionIds,
timeLocks,
timeOuts,
...extraArgs,
],
from,
)
}
public getConditionTypes(): Promise<string[]> {
return this.call("getConditionTypes", [])
}
public async getConditions(): Promise<Condition[]> {
const keeper = await Keeper.getInstance()
return (await this.getConditionTypes())
.map(address => keeper.getConditionByAddress(address))
}
}

View File

@ -0,0 +1,28 @@
import { AgreementTemplate } from "./AgreementTemplate.abstract"
export class EscrowAccessSecretStoreTemplate extends AgreementTemplate {
public static async getInstance(): Promise<EscrowAccessSecretStoreTemplate> {
return AgreementTemplate.getInstance("EscrowAccessSecretStoreTemplate", EscrowAccessSecretStoreTemplate)
}
public createAgreement(
agreementId: string,
did: string,
conditionIds: string[],
timeLocks: number[],
timeOuts: number[],
accessConsumer: string,
from?: string,
) {
return super.createAgreement(
agreementId,
did,
conditionIds,
timeLocks,
timeOuts,
[accessConsumer],
from,
)
}
}

View File

@ -0,0 +1,2 @@
export * from './AgreementTemplate.abstract'
export { EscrowAccessSecretStoreTemplate } from './EscrowAccessSecretStoreTemplate'

View File

@ -115,7 +115,7 @@ export default class ServiceAgreement extends OceanBase {
publisher: Account,
): Promise<ServiceAgreement> {
const {serviceAgreement} = await Keeper.getInstance()
const {serviceAgreement} = <any>await Keeper.getInstance()
const service = ddo.findServiceById<"Access">(serviceDefinitionId)
@ -228,7 +228,7 @@ export default class ServiceAgreement extends OceanBase {
}
public async payAsset(assetId: string, price: number, consumer: Account): Promise<boolean> {
const {paymentConditions, token} = await Keeper.getInstance()
const {paymentConditions, token} = <any>await Keeper.getInstance()
await token.approve(paymentConditions.getAddress(), price, consumer.getId())
@ -238,7 +238,7 @@ export default class ServiceAgreement extends OceanBase {
}
public async grantAccess(documentId: string, publisher: Account): Promise<boolean> {
const {accessConditions} = await Keeper.getInstance()
const {accessConditions} = <any>await Keeper.getInstance()
const grantAccessReceipt =
await accessConditions.grantAccess(this.getId(), documentId, publisher.getId())

View File

@ -39,7 +39,7 @@ export default class ServiceAgreementTemplate extends OceanBase {
.map((method: Method, i: number) => method.isTerminalCondition ? i : null)
.filter((index: number) => index !== null)
const {serviceAgreement} = await Keeper.getInstance()
const {serviceAgreement} = <any>await Keeper.getInstance()
const owner = await this.getOwner()
@ -91,12 +91,12 @@ export default class ServiceAgreementTemplate extends OceanBase {
* gets the status of a service agreement template
*/
public async getStatus(): Promise<boolean> {
const {serviceAgreement} = await Keeper.getInstance()
const {serviceAgreement} = <any>await Keeper.getInstance()
return serviceAgreement.getTemplateStatus(this.getId())
}
public async getOwner(): Promise<Account> {
const {serviceAgreement} = await Keeper.getInstance()
const {serviceAgreement} = <any>await Keeper.getInstance()
return new Account(await serviceAgreement.getTemplateOwner(this.id))
}

View File

@ -2,21 +2,21 @@ import Config from "./models/Config"
import Account from "./ocean/Account"
import DID from "./ocean/DID"
import Ocean from "./ocean/Ocean"
import ServiceAgreement from "./ocean/ServiceAgreements/ServiceAgreement"
import ServiceAgreementTemplate from "./ocean/ServiceAgreements/ServiceAgreementTemplate"
import Access from "./ocean/ServiceAgreements/Templates/Access"
import FitchainCompute from "./ocean/ServiceAgreements/Templates/FitchainCompute"
import SecretStoreProvider from "./secretstore/SecretStoreProvider"
import Logger from "./utils/Logger"
import WebServiceConnectorProvider from "./utils/WebServiceConnectorProvider"
import Keeper from "./keeper/Keeper"
import EventListener from "./keeper/EventListener"
import * as templates from "./keeper/contracts/templates"
import * as conditions from "./keeper/contracts/conditions"
// Exports
export * from "./ddo/DDO"
export * from "./ddo/MetaData"
export { generateId } from "./utils/GeneratorHelpers"
const Templates = {Access, FitchainCompute}
export { AgreementTemplate } from "./keeper/contracts/templates"
export { Condition } from "./keeper/contracts/conditions"
export {
Ocean,
@ -25,10 +25,10 @@ export {
Config,
DID,
EventListener,
Keeper,
Logger,
SecretStoreProvider,
ServiceAgreement,
ServiceAgreementTemplate,
Templates,
WebServiceConnectorProvider,
conditions,
templates,
}