MDV es una solucion descentralizada de solicitudes de documentos que permite crear workflows de tramites por medio de Solidity
y Javascript
.
Adicionalmente incluye soporte para PKCS#7
para documentos PDF e integracion de identidad digital por medio de ethr-did
.
WTemplate
define la plantilla madre de la solucion, esta se encarga de:
- Creacion de la secuencia de pasos
- Validacion y ejecuccion de pasos
- Roles
- Administracion de subscripciones de usuarios
Un modelo requiere heredar de IWModel
, esta interfaz es usada por WTemplate
para acceder e interactuar con el modelo.
WFStorage
contiene validadores y otros utilitarios.
El registro de extension es el contrato del API de Extensiones.
- Approvals:
0x7740A63CF6FC3827589861f7D3a1Ca6307a59480
- ExtensionEventRegistry:
0xfA72D80472b10e5f315e8f9d4652C0f8d3E31EE7
- WTemplate:
0x82f22A2bf1741fD83850ffd79F2bd82fcC5863BD
Pronto
Un WTemplate
consta de los siguientes componentes:
Actores
: Los diferentes tipos de personas en un flujo.Pasos
: Las transiciones entre los estados del flujo, contiene ademas ramificaciones, extensiones y validaciones.Modelo
: El modelo implementa la logica de negocios del flujo, aplicando un API CRU (Create Read Update).
Para crear una nueva plantilla, se utiliza el API Javascript WFactoryHelper
.
// Factory helper
let wf = new WFactoryHelper();
wf.createActors(['USER', 'NOTARY']);
wf.createStates([
'NONE',
'CREATED',
'ACCEPTED',
'CERTIFIED',
'COMPLETED',
'MULTI_SIGNERS',
]);
- currentActor: Actor cual ejecuta el paso actual
- current: Paso actual
- next: Siguiente paso
- mappingType: 0 = Crear, 2 = Actualizar
- forkId: Ramificacion, inicia en 1
- stepValidations: Validaciones de estados, usando un operador OR
- senderValidations: Validaciones de persona que envia la solicitud
- recipientValidations: Validaciones de recipientes
wf.createStep({
currentActor: wf.getActor('USER'),
current: wf.getState('NONE'), // none
next: wf.getState('CREATED'), // created
mappingType: 0, // init
});
wf.createStep({
currentActor: wf.getActor('NOTARY'),
current: wf.getState('CREATED'), // created
next: wf.getState('ACCEPTED'), // accepted
mappingType: 2, // status
stepValidations: [wf.getState('CREATED')],
});
wf.createStep({
currentActor: wf.getActor('NOTARY'),
current: wf.getState('ACCEPTED'), // accepted
next: wf.getState('MULTI_SIGNERS'), // certified
mappingType: 2, // updated_diff
forkId: wf.getState('CERTIFIED'),
stepValidations: [wf.getState('ACCEPTED')],
recipientValidations: ['0x1198258023eD0D6fae5DBCF3Af2aeDaaA363571r'],
});
wf.createStep({
currentActor: wf.getActor('NOTARY'),
current: wf.getState('MULTI_SIGNERS'), // accepted
next: wf.getState('CERTIFIED'), // certified
mappingType: 2, // updated_diff
forkId: wf.getState('MULTI_SIGNERS'),
stepValidations: [wf.getState('ACCEPTED'), wf.getState('MULTI_SIGNERS')],
recipientValidations: ['0x1198258023eD0D6fae5DBCF3Af2aeDaaA363571r','0xaaC58E89996496640c8b5898A7e0218E9b6E90cR'],
});
wf.createStep({
currentActor: wf.getActor('USER'),
current: wf.getState('CERTIFIED'), // forkId: 0,
next: wf.getState('COMPLETED'), // completed
mappingType: 2, // status
stepValidations: [wf.getState('CERTIFIED')],
senderValidations: ['0xaaC58E89996496640c8b5898A7e0218E9b6E90cR'],
});
El modelo implementa onAdd
y updateSwitch
que son llamados por WTemplate
. Cada modelo es recomendable tenga uno o mas structs
que definen el modelo de datos enviados en el payload
.
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "./WStep.sol";
import "./WFStorage.sol";
import './ExtensionEventRegistry.sol';
import './IExtension.sol';
import "../../node_modules/@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract TestDocumentModel is WFStorage, ExtensionEventRegistry, ReentrancyGuard {
address owner;
struct TestDocument {
string fileIpfsJson;
string certifiedFilesIpfsJson;
string email;
string description;
string name;
string lastname;
uint256 amount;
// system
address user;
address recipient;
bytes32 status;
}
mapping (uint256 => TestDocument) public table;
ExtensionEventRegistry private extReg;
constructor(address r) public {
owner = msg.sender;
extReg = ExtensionEventRegistry(r);
}
function getStructAsTuple(bytes calldata data) external returns(string memory, string memory, string memory, string memory,
string memory, string memory, uint256) {
return abi.decode(data, (string, string, string, string, string, string, uint256));
}
function getStruct(bytes calldata data) external returns(TestDocument memory) {
(string memory a, string memory b, string memory c, string memory d,
string memory e, string memory f, uint256 g) = abi.decode(data, (string, string, string, string, string, string, uint256));
return TestDocument(a, b, c, d, e, f, g, address(0), address(0), bytes32(0));
}
function onAddRow(
address to,
address sender,
uint256 next,
uint256 current,
bytes memory fields
) public nonReentrant returns (bool) {
currentDocumentIndex = rowCount;
rowCount = rowCount + 1;
table[rowCount-1] = this.getStruct(fields);
table[rowCount-1].user = sender;
table[rowCount-1].recipient = to;
table[rowCount-1].status = keccak256(abi.encodePacked(stepsByInitState[next].current));
return true;
}
function onUpdate(uint256 doc,
address to,
address sender,
uint256 next,
uint256 current,
TestDocument memory document
) public returns (bool) {
validateSender(current, sender);
validateRecipient(current, to);
validateStatus(current, stepsByInitState[current].current);
currentDocumentIndex = doc;
table[doc].status = keccak256(abi.encodePacked(stepsByInitState[next].current));
if (keccak256(abi.encodePacked(table[doc].certifiedFilesIpfsJson)) != keccak256(abi.encodePacked(""))) {
table[doc].certifiedFilesIpfsJson = document.certifiedFilesIpfsJson;
}
return true;
}
function onExtensionUpdate(uint256 doc,
address to,
address sender,
uint256 next,
uint256 current, uint256 extensionEvtId,
TestDocument memory document
) public nonReentrant returns (bool, uint256) {
validateStatus(next, stepsByInitState[current].current);
uint256 calculatedNextFork = stepsByInitState[current].forkId;
IExtension ext = IExtension(extReg.read(extensionEvtId).extContract);
if (ext.canExec(extReg.read(extensionEvtId).id, sender) && keccak256(bytes(extReg.read(extensionEvtId).name)) == keccak256(bytes("approvals"))) {
if (ext.executeExtension(extReg.read(extensionEvtId).id, sender, abi.encodePacked(document.amount))) {
table[doc].status = keccak256(abi.encodePacked(stepsByInitState[current].next));
} else {
calculatedNextFork = stepsByInitState[current].forkId;
table[doc].status = keccak256(abi.encodePacked( stepsByInitState[current].forkId));
}
}
currentDocumentIndex = doc;
if (keccak256(abi.encodePacked(table[doc].certifiedFilesIpfsJson)) != keccak256(abi.encodePacked(""))) {
table[doc].certifiedFilesIpfsJson = document.certifiedFilesIpfsJson;
}
return (true, calculatedNextFork);
}
function updateSwitch(uint256 doc,
address to,
address sender,
uint256 next,
uint256 current,
uint256 extensionEvtId,
bytes memory fields
) public returns (bool, uint256) {
uint256 calculatedNextFork = next;
if (extensionEvtId > 0) {
(bool ok, uint256 fork) = this.onExtensionUpdate(doc, to, sender, next, current, extensionEvtId, this.getStruct(fields));
calculatedNextFork = fork;
} else {
this.onUpdate(doc, to, sender, next, current, this.getStruct(fields));
}
return (true, calculatedNextFork);
}
}
Este payload se codifica en bytes y es creado de este modo:
const payload = wf.createPayload({
fileIpfsJson: JSON.stringify([{ path: '', content: '' }]),
certifiedFilesIpfsJson: '',
email: '',
description: 'Testing workflow',
name: 'John',
lastname: 'Lopez',
amount: 100,
});
Para crear la plantilla, definimos cada secuencia utilizando wf.tightLinkSteps
y ejecutamos wf.createWF
:
// 7) Create WF
await template.createWF(
wf.tightLinkSteps([
[wf.getActor('USER'), wf.getState('NONE'), wf.getState('CREATED')],
[wf.getActor('NOTARY'), wf.getState('CREATED'), wf.getState('ACCEPTED')],
[
wf.getActor('NOTARY'),
wf.getState('ACCEPTED'),
wf.getState('MULTI_SIGNERS'),
],
[
wf.getActor('NOTARY'),
wf.getState('MULTI_SIGNERS'),
wf.getState('CERTIFIED'),
],
[wf.getActor('USER'), wf.getState('CERTIFIED'), wf.getState('COMPLETED')],
]),
wf.getSteps()
);
Soporte para el metodo DID ethr-did
, lo cual permite acceder a los features del ethr-did-resolver
(delegacion, reasignacion, etc)
Soporte para X509 PKCS#7
en modo detached. El firmado se realiza desde un cliente web, y sube al storage descentralizado el documento y firma criptografica.
Un API backend valida la firma del documento con la llave publica del firmante.
Soporte para Swarm Ethereum
que ofrece encriptacion en reposo integrado, lo cual permite mas eficiencia al no tener que procesar por servicios intermediarios.