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

Merge pull request #55 from oceanprotocol/fix/lockPayment

Fix/lock payment
This commit is contained in:
Sebastian Gerske 2018-11-22 13:35:32 +01:00 committed by GitHub
commit 34b3b23dd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 259 additions and 1906 deletions

1794
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -48,7 +48,7 @@
},
"homepage": "https://github.com/oceanprotocol/squid-js#readme",
"dependencies": {
"@oceanprotocol/keeper-contracts": "0.3.23",
"@oceanprotocol/keeper-contracts": "0.3.25",
"@oceanprotocol/secret-store-client": "0.0.12",
"bignumber.js": "^8.0.1",
"eth-crypto": "^1.2.7",
@ -64,7 +64,7 @@
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.7",
"@types/node": "^10.12.10",
"chai": "^4.2.0",
"mocha": "^5.2.0",
"nyc": "^13.1.0",

View File

@ -60,16 +60,18 @@ export default abstract class ContractBase {
if (!this.contract.methods[name]) {
throw new Error(`Method "${name}" is not part of contract "${this.contractName}"`)
}
// Logger.log(name, args)
const method = this.contract.methods[name]
try {
const tx = method(...args)
const estimatedGas = await tx.estimateGas(args, {
const methodInstance = method(...args)
const estimatedGas = await methodInstance.estimateGas(args, {
from,
})
return tx.send({
const tx = methodInstance.send({
from,
gas: estimatedGas,
})
return tx
} catch (err) {
const mappedArgs = this.searchMethod(name).inputs.map((input, i) => {
return {
@ -87,6 +89,7 @@ export default abstract class ContractBase {
if (!this.contract.methods[name]) {
throw new Error(`Method ${name} is not part of contract ${this.contractName}`)
}
// Logger.log(name)
try {
const method = this.contract.methods[name](...args)
return method.call(from ? {from} : null)

View File

@ -10,15 +10,18 @@ export default class ServiceAgreement extends ContractBase {
return serviceAgreement
}
public async setupAgreementTemplate(templateId: string, methodReflections: MethodReflection[],
dependencyMatrix: number[], name: any, fulfillmentIndices: number[],
public async setupAgreementTemplate(templateId: string,
methodReflections: MethodReflection[],
dependencyMatrix: number[],
name: any,
fulfillmentIndices: number[],
fulfillmentOperator: number,
ownerAddress: string)
: Promise<Receipt> {
ownerAddress: string): Promise<Receipt> {
return this.send("setupAgreementTemplate", ownerAddress, [
templateId, methodReflections.map((r) => r.address),
methodReflections.map((r) => r.signature), dependencyMatrix, name, fulfillmentIndices, fulfillmentOperator,
methodReflections.map((r) => r.signature), dependencyMatrix, name, fulfillmentIndices,
fulfillmentOperator,
])
}

View File

@ -13,7 +13,6 @@ import Service from "../ddo/Service"
import Keeper from "../keeper/Keeper"
import Web3Provider from "../keeper/Web3Provider"
import Config from "../models/Config"
import InputType from "../models/InputType"
import ValueType from "../models/ValueType"
import SecretStoreProvider from "../secretstore/SecretStoreProvider"
import Logger from "../utils/Logger"
@ -107,11 +106,11 @@ export default class Ocean {
return null
}
const parameters: Parameter[] = condition.methodReflection.inputs.map((input: InputType) => {
const parameters: Parameter[] = condition.parameters.map((parameter: Parameter) => {
return {
name: input.name,
type: input.type,
value: mapParameterValueToName(input.name),
name: parameter.name,
type: parameter.type,
value: mapParameterValueToName(parameter.name),
} as Parameter
})

View File

@ -1,4 +1,5 @@
import MethodReflection from "../../models/MethodReflection"
import Parameter from "./Parameter"
export default class Condition {
public methodReflection: MethodReflection
@ -7,4 +8,5 @@ export default class Condition {
public dependencyTimeoutFlags: number[]
public isTerminalCondition: boolean
public timeout: number
public parameters: Parameter[]
}

View File

@ -1,8 +1,11 @@
import Parameter from "./Parameter"
export default class Method {
public name: string
public contractName: string
public methodName: string
public timeout: number
public parameters: Parameter[]
public dependencies: string[]
public dependencyTimeoutFlags: number[]
public isTerminalCondition: boolean

View File

@ -0,0 +1,4 @@
export default class Parameter {
public name: string
public type: string
}

View File

@ -19,18 +19,12 @@ export default class ServiceAgreement extends OceanBase {
// Logger.log("signing SA", serviceAgreementId)
const service: Service = ddo.findServiceById(serviceDefinitionId)
const values: ValuePair[] = ServiceAgreement.getValuesFromService(service, serviceAgreementId)
const valueHashes = ServiceAgreement.createValueHashes(values)
const values: ValuePair[][] = ServiceAgreement.getValuesFromService(service, serviceAgreementId)
const valueHashes: string[] = ServiceAgreement.createValueHashes(values)
const timeoutValues: number[] = ServiceAgreement.getTimeoutValuesFromService(service)
const serviceAgreementHashSignature = await ServiceAgreement
.createSAHashSignature(
service,
serviceAgreementId,
values,
valueHashes,
timeoutValues,
consumer)
const serviceAgreementHashSignature = await ServiceAgreement.createSAHashSignature(service, serviceAgreementId,
valueHashes, timeoutValues, consumer)
return serviceAgreementHashSignature
}
@ -46,8 +40,8 @@ export default class ServiceAgreement extends OceanBase {
// Logger.log("executing SA", serviceAgreementId)
const service: Service = ddo.findServiceById(serviceDefinitionId)
const values: ValuePair[] = ServiceAgreement.getValuesFromService(service, serviceAgreementId)
const valueHashes = ServiceAgreement.createValueHashes(values)
const values: ValuePair[][] = ServiceAgreement.getValuesFromService(service, serviceAgreementId)
const valueHashes: string[] = ServiceAgreement.createValueHashes(values)
const timeoutValues: number[] = ServiceAgreement.getTimeoutValuesFromService(service)
// todo get consumer from ddo
@ -60,11 +54,9 @@ export default class ServiceAgreement extends OceanBase {
private static async createSAHashSignature(service: Service,
serviceAgreementId: string,
values: ValuePair[],
valueHashes: string[],
timeoutValues: number[],
consumer: Account):
Promise<string> {
consumer: Account): Promise<string> {
if (!service.templateId) {
throw new Error("TemplateId not found in ddo.")
@ -74,13 +66,16 @@ export default class ServiceAgreement extends OceanBase {
return condition.conditionKey
})
const serviceAgreementHash = ServiceAgreement
.hashServiceAgreement(
service.templateId,
serviceAgreementId,
conditionKeys,
valueHashes,
timeoutValues)
if (conditionKeys.length !== valueHashes.length) {
throw new Error("Hashing SA failed!")
}
const serviceAgreementHash = ServiceAgreement.hashServiceAgreement(
service.templateId,
serviceAgreementId,
conditionKeys,
valueHashes,
timeoutValues)
const serviceAgreementHashSignature = await Web3Provider
.getWeb3().eth.sign(serviceAgreementHash, consumer.getId())
@ -130,18 +125,31 @@ export default class ServiceAgreement extends OceanBase {
)
}
private static createValueHashes(valuePairs: ValuePair[]): any[] {
return valuePairs.map((valuePair) => {
return ServiceAgreement.hashSingleValue(valuePair)
private static createValueHashes(parameterValuePairs: ValuePair[][]): string[] {
const hashes: string[] = []
parameterValuePairs.map((valuePairs: ValuePair[]) => {
hashes.push(ServiceAgreement.hashValuePairArray(valuePairs))
})
return hashes
}
private static hashSingleValue(data: ValuePair): string {
private static hashValuePairArray(valuePairs: ValuePair[]): string {
let hash: string
try {
return Web3Provider.getWeb3().utils.soliditySha3(data).toString("hex")
hash = Web3Provider.getWeb3().utils.soliditySha3(...valuePairs).toString("hex")
} catch (err) {
Logger.error(`Hashing of ${JSON.stringify(data, null, 2)} failed.`)
Logger.error(`Hashing of ${JSON.stringify(valuePairs, null, 2)} failed.`)
throw err
}
if (!hash) {
throw new Error("hashValuePairArray failed to create hash.")
}
return hash
}
private static hashServiceAgreement(serviceAgreementTemplateId: string,
@ -168,20 +176,22 @@ export default class ServiceAgreement extends OceanBase {
return timeoutValues
}
private static getValuesFromService(service: Service, serviceAgreementId: string): ValuePair[] {
private static getValuesFromService(service: Service, serviceAgreementId: string): ValuePair[][] {
const values: ValuePair[] = []
const values: ValuePair[][] = []
service.conditions.forEach((condition) => {
service.conditions.forEach((condition, i) => {
const contionValues: ValuePair[] = []
condition.parameters.forEach((parameter) => {
values.push({
contionValues.push({
type: parameter.type,
value: parameter.name === "serviceId" ? "0x" + serviceAgreementId : parameter.value,
} as ValuePair)
})
})
// Logger.log("Values", JSON.stringify(values, null, 2))
values[i] = contionValues
})
return values
}
@ -195,14 +205,16 @@ export default class ServiceAgreement extends OceanBase {
super(serviceAgreementId)
}
public async lockPayment(assetId: string, price: number, consumer: Account): Promise<boolean> {
const {paymentConditions} = await Keeper.getInstance()
public async buyAsset(assetId: string, price: number, consumer: Account): Promise<boolean> {
const {paymentConditions, token} = await Keeper.getInstance()
const lockPaymentRceipt =
await token.approve(paymentConditions.getAddress(), price, consumer.getId())
const lockPaymentReceipt =
await paymentConditions.lockPayment(this.getId(), assetId, price,
consumer.getId())
return lockPaymentRceipt.status
return lockPaymentReceipt.status
}
public async grantAccess(assetId: string, documentId: string): Promise<boolean> {
@ -212,7 +224,7 @@ export default class ServiceAgreement extends OceanBase {
await accessConditions.grantAccess(this.getId(), assetId, documentId,
this.publisher.getId())
return grantAccessReceipt.status
return !!grantAccessReceipt.events.AccessGranted
}
public async getStatus() {

View File

@ -12,8 +12,8 @@ import TemplateBase from "./Templates/TemplateBase"
export default class ServiceAgreementTemplate extends OceanBase {
private static generateConditionsKey(serviceAgreementTemplateId: string, methodReflection: MethodReflection)
: string {
private static generateConditionsKey(serviceAgreementTemplateId: string,
methodReflection: MethodReflection): string {
const values = [
{type: "bytes32", value: serviceAgreementTemplateId} as ValuePair,
{type: "address", value: methodReflection.address} as ValuePair,
@ -35,13 +35,11 @@ export default class ServiceAgreementTemplate extends OceanBase {
}))
const fulfillmentIndices: number[] = this.template.Methods
.map((method: Method, i: number) => method.isTerminalCondition ? i : undefined)
.filter((index: number) => index !== undefined)
.map((method: Method, i: number) => method.isTerminalCondition ? i : null)
.filter((index: number) => index !== null)
const {serviceAgreement} = await Keeper.getInstance()
const methodReflections = await this.getMethodReflections()
const owner = await this.getOwner()
if (owner.getId() === templateOwnerAddress) {
@ -58,7 +56,7 @@ export default class ServiceAgreementTemplate extends OceanBase {
const receipt = await serviceAgreement
.setupAgreementTemplate(
this.template.id,
methodReflections,
await this.getMethodReflections(),
dependencyMatrix,
Web3Provider.getWeb3().utils.fromAscii(this.template.templateName),
fulfillmentIndices,
@ -106,6 +104,7 @@ export default class ServiceAgreementTemplate extends OceanBase {
return {
methodReflection,
timeout: method.timeout,
parameters: method.parameters,
dependencies: method.dependencies,
dependencyTimeoutFlags: method.dependencyTimeoutFlags,
isTerminalCondition: method.isTerminalCondition,
@ -123,23 +122,32 @@ export default class ServiceAgreementTemplate extends OceanBase {
throw new Error("Deps and timeouts need the same length")
}
// map name to index
const mappedDependencies: number[] = dependencies.map((dep: string) => {
return this.template.Methods.findIndex((m) => m.name === dep)
const mappedDependencies: number[] = []
const mappedDependencyTimeoutFlags: number[] = []
this.template.Methods.forEach((m, i) => {
const di = dependencies.findIndex((d) => d === m.name)
mappedDependencies.push(di > -1 ? 1 : 0)
mappedDependencyTimeoutFlags.push((di > -1 && dependencyTimeoutFlags[di]) ? 1 : 0)
})
let compressedDependencyValue = 0
const numBits = 2 // 1st for dependency, 2nd for timeout flag
for (let i = 0; i < mappedDependencies.length; i++) {
const dependencyIndex = mappedDependencies[i]
const timeout = dependencyTimeoutFlags[i]
const offset = i * numBits
// tslint:disable-next-line
compressedDependencyValue |= dependencyIndex * 2 ** (offset + 0) // the dependency bit
// tslint:disable-next-line
compressedDependencyValue |= timeout * 2 ** (offset + 1) // the timeout bit
if (mappedDependencies.length !== mappedDependencyTimeoutFlags.length) {
throw new Error("Deps and timeouts need the same length")
}
// Logger.log(dependencies, mappedDependencies, dependencyTimeoutFlags, mappedDependencyTimeoutFlags)
let compressedDependencyValue: number = 0
const numBits: number = 2 // 1st for dependency, 2nd for timeout flag
mappedDependencies.forEach((d: number, i: number) => {
const t: number = mappedDependencyTimeoutFlags[i]
const offset: number = i * numBits
// tslint:disable-next-line
compressedDependencyValue |= d * 2 ** (offset + 0) // the dependency bit
// tslint:disable-next-line
compressedDependencyValue |= t * 2 ** (offset + 1) // the timeout bit
})
return compressedDependencyValue
}

View File

@ -11,6 +11,16 @@ export default class Access extends TemplateBase {
contractName: "PaymentConditions",
methodName: "lockPayment",
timeout: 0,
parameters: [
{
name: "assetId",
type: "bytes32",
},
{
name: "price",
type: "uint256",
},
],
dependencies: [],
dependencyTimeoutFlags: [],
isTerminalCondition: false,
@ -20,6 +30,16 @@ export default class Access extends TemplateBase {
contractName: "AccessConditions",
methodName: "grantAccess",
timeout: 10,
parameters: [
{
name: "assetId",
type: "bytes32",
},
{
name: "documentKeyId",
type: "bytes32",
},
],
dependencies: ["lockPayment"],
dependencyTimeoutFlags: [0],
isTerminalCondition: false,
@ -29,6 +49,16 @@ export default class Access extends TemplateBase {
contractName: "PaymentConditions",
methodName: "releasePayment",
timeout: 10,
parameters: [
{
name: "assetId",
type: "bytes32",
},
{
name: "price",
type: "uint256",
},
],
dependencies: ["grantAccess"],
dependencyTimeoutFlags: [0],
isTerminalCondition: true,
@ -38,6 +68,16 @@ export default class Access extends TemplateBase {
contractName: "PaymentConditions",
methodName: "refundPayment",
timeout: 10,
parameters: [
{
name: "assetId",
type: "bytes32",
},
{
name: "price",
type: "uint256",
},
],
dependencies: ["lockPayment", "grantAccess"],
dependencyTimeoutFlags: [0, 1],
isTerminalCondition: true,

View File

@ -7,7 +7,6 @@ import EventHandlers from "../../src/ddo/EventHandlers"
import MetaData from "../../src/ddo/MetaData"
import Parameter from "../../src/ddo/Parameter"
import Service from "../../src/ddo/Service"
import InputType from "../../src/models/InputType"
import Account from "../../src/ocean/Account"
import IdGenerator from "../../src/ocean/IdGenerator"
import Ocean from "../../src/ocean/Ocean"
@ -49,58 +48,61 @@ describe("ServiceAgreement", () => {
const conditions: Condition[] = await serviceAgreementTemplate.getConditions()
// create ddo conditions out of the keys
const ddoConditions: DDOCondition[] = conditions.map((condition, index): DDOCondition => {
const ddoConditions: DDOCondition[] = conditions
.map((condition: Condition, index): DDOCondition => {
const events: Event[] = [
{
name: "PaymentReleased",
actorType: [
"consumer",
],
handlers: {
moduleName: "serviceAgreement",
functionName: "fulfillAgreement",
version: "0.1",
} as EventHandlers,
} as Event,
]
const events: Event[] = [
{
name: "PaymentReleased",
actorType: [
"consumer",
],
handlers: {
moduleName: "serviceAgreement",
functionName: "fulfillAgreement",
version: "0.1",
} as EventHandlers,
} as Event,
]
const mapParameterValueToName = (name) => {
const mapParameterValueToName = (name) => {
switch (name) {
case "price":
return metadata.base.price
case "assetId":
return "0x" + assetId
case "documentKeyId":
return "0x1234"
switch (name) {
case "price":
return metadata.base.price
case "assetId":
return "0x" + assetId
case "documentKeyId":
return "0x" + assetId
}
return null
}
return null
}
const parameters: Parameter[] = condition.parameters
.map((parameter: Parameter) => {
return {
name: parameter.name,
type: parameter.type,
value: mapParameterValueToName(parameter.name),
} as Parameter
})
const parameters: Parameter[] = condition.methodReflection.inputs.map((input: InputType) => {
return {
name: input.name,
type: input.type,
value: mapParameterValueToName(input.name),
} as Parameter
contractName: condition.methodReflection.contractName,
methodName: condition.methodReflection.methodName,
timeout: condition.timeout,
index,
conditionKey: condition.condtionKey,
parameters,
events,
dependencies: condition.dependencies,
dependencyTimeoutFlags: condition.dependencyTimeoutFlags,
isTerminalCondition: condition.isTerminalCondition,
} as DDOCondition
})
return {
contractName: condition.methodReflection.contractName,
methodName: condition.methodReflection.methodName,
timeout: condition.timeout,
index,
conditionKey: condition.condtionKey,
parameters,
events,
dependencies: condition.dependencies,
dependencyTimeoutFlags: condition.dependencyTimeoutFlags,
isTerminalCondition: condition.isTerminalCondition,
} as DDOCondition
})
accessService = {
type: "Access",
serviceDefinitionId: IdGenerator.generateId(),
@ -117,8 +119,7 @@ describe("ServiceAgreement", () => {
describe("#signServiceAgreement()", () => {
it("should sign an service agreement", async () => {
const id: string = IdGenerator.generateId()
const did: string = `did:op:${id}`
const did: string = `did:op:${assetId}`
const ddo = new DDO({id: did, service: [accessService]})
const serviceAgreementId: string = IdGenerator.generateId()
@ -137,8 +138,7 @@ describe("ServiceAgreement", () => {
describe("#executeServiceAgreement()", () => {
it("should execute an service agreement", async () => {
const id: string = IdGenerator.generateId()
const did: string = `did:op:${id}`
const did: string = `did:op:${assetId}`
const ddo = new DDO({id: did, service: [accessService]})
const serviceAgreementId: string = IdGenerator.generateId()
@ -162,8 +162,7 @@ describe("ServiceAgreement", () => {
describe("#getStatus()", () => {
it("should get the status of a newly created service agreement", async () => {
const id: string = IdGenerator.generateId()
const did: string = `did:op:${id}`
const did: string = `did:op:${assetId}`
const ddo = new DDO({id: did, service: [accessService]})
const serviceAgreementId: string = IdGenerator.generateId()
@ -184,16 +183,16 @@ describe("ServiceAgreement", () => {
})
})
describe("#lockPayment()", () => {
xit("should lock the payment in that service agreement", async () => {
describe("#buyAsset()", () => {
it("should lock the payment in that service agreement", async () => {
const id: string = IdGenerator.generateId()
const did: string = `did:op:${id}`
const did: string = `did:op:${assetId}`
const ddo = new DDO({id: did, service: [accessService, metaDataService]})
const serviceAgreementId: string = IdGenerator.generateId()
// @ts-ignore
WebServiceConnectorProvider.setConnector(new WebServiceConnectorMock(ddo))
const serviceAgreementSignature: string =
await ServiceAgreement.signServiceAgreement(assetId, ddo, accessService.serviceDefinitionId,
serviceAgreementId, consumerAccount)
@ -204,17 +203,19 @@ describe("ServiceAgreement", () => {
serviceAgreementId, serviceAgreementSignature, consumerAccount, publisherAccount)
assert(serviceAgreement)
const paid: boolean = await serviceAgreement.lockPayment(assetId, metaDataService.metadata.base.price,
// get funds
await consumerAccount.requestTokens(metaDataService.metadata.base.price)
const paid: boolean = await serviceAgreement.buyAsset(assetId, metaDataService.metadata.base.price,
consumerAccount)
assert(paid)
})
})
describe("#grantAccess()", () => {
xit("should grant access in that service agreement", async () => {
it("should grant access in that service agreement", async () => {
const id: string = IdGenerator.generateId()
const did: string = `did:op:${id}`
const did: string = `did:op:${assetId}`
const ddo = new DDO({id: did, service: [accessService]})
const serviceAgreementId: string = IdGenerator.generateId()
@ -230,11 +231,39 @@ describe("ServiceAgreement", () => {
serviceAgreementId, serviceAgreementSignature, consumerAccount, publisherAccount)
assert(serviceAgreement)
const paid: boolean = await serviceAgreement.lockPayment(assetId, 10, consumerAccount)
// get funds
await consumerAccount.requestTokens(metaDataService.metadata.base.price)
const paid: boolean = await serviceAgreement.buyAsset(assetId, metaDataService.metadata.base.price,
consumerAccount)
assert(paid)
const accessGranted: boolean = await serviceAgreement.grantAccess(assetId, IdGenerator.generateId())
// todo: use document id
const accessGranted: boolean = await serviceAgreement.grantAccess(assetId, assetId)
assert(accessGranted)
})
it("should fail to grant grant access if there is no payment", async () => {
const did: string = `did:op:${assetId}`
const ddo = new DDO({id: did, service: [accessService]})
const serviceAgreementId: string = IdGenerator.generateId()
// @ts-ignore
WebServiceConnectorProvider.setConnector(new WebServiceConnectorMock(ddo))
const serviceAgreementSignature: string =
await ServiceAgreement.signServiceAgreement(assetId, ddo, accessService.serviceDefinitionId,
serviceAgreementId, consumerAccount)
assert(serviceAgreementSignature)
const serviceAgreement: ServiceAgreement =
await ServiceAgreement.executeServiceAgreement(assetId, ddo, accessService.serviceDefinitionId,
serviceAgreementId, serviceAgreementSignature, consumerAccount, publisherAccount)
assert(serviceAgreement)
// todo: use document id
const accessGranted: boolean = await serviceAgreement.grantAccess(assetId, assetId)
assert(!accessGranted)
})
})
})